mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 875ebdcfb0 | |||
| 2e07799250 | |||
| 0d9d21a5c1 | |||
| 836e12b1c5 | |||
| 7538aeae0e | |||
| 3e9ba9ca8e | |||
| e4c296a7d2 | |||
| 44748270da | |||
| 8b1dc4c43d | |||
| c105c6da38 | |||
| 3001420a1d | |||
| 432ff444a1 | |||
| aa3c88a6c4 | |||
| 601a98e649 | |||
| 23c7d19ef0 | |||
| 59c49b5833 | |||
| 2cca0bd819 | |||
| fbf4c4add2 | |||
| 78d6ff735e | |||
| a7f51fa666 | |||
| 5175f95289 | |||
| 3c9f4a8ae9 | |||
| 310730782c | |||
| 77a413d750 | |||
| fe31f93c94 | |||
| 9cc0f88e02 | |||
| 412fda10fd | |||
| aa441274c7 | |||
| ba20267151 | |||
| aabb7559b8 | |||
| 606d14a399 | |||
| c049022798 | |||
| cae32c5bd4 |
@@ -451,5 +451,6 @@ console.log(status(component));
|
||||
// logs either:
|
||||
// - 'new', if the component is new and has not been mounted yet
|
||||
// - 'mounted', if the component is currently mounted
|
||||
// - 'cancelled', if the component has not been mounted yet but will be destroyed soon
|
||||
// - 'destroyed' if the component is currently destroyed
|
||||
```
|
||||
|
||||
@@ -175,7 +175,7 @@ class Notebook extends Component {
|
||||
<div class="notebook">
|
||||
<div class="tabs">
|
||||
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
|
||||
<t t-esc="props.slots[tab].title"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
@@ -376,15 +376,16 @@ An important difference should be made with the usual `QWeb` behaviour: Owl
|
||||
requires the presence of a `t-key` directive, to be able to properly reconcile
|
||||
renderings.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
`t-foreach` can iterate on any iterable, and also has special support for objects
|
||||
and maps, it will expose the key of the current iteration as the contents of the
|
||||
`t-as`, and the corresponding value with the same name and the suffix `_value`.
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced with the name
|
||||
passed to `t-as`):
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other useful
|
||||
variables (note: `$as` will be replaced with the name passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_value`: the current iteration value, identical to `$as` for arrays and
|
||||
other iterables, but for objects and maps, it provides the value (where `$as`
|
||||
provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
|
||||
+1
-2
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="assets/milligram.css">
|
||||
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
||||
<link rel="stylesheet" href="assets/main.css">
|
||||
<script src="./owl.js"></script>
|
||||
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
@@ -68,7 +68,6 @@
|
||||
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
||||
</footer>
|
||||
<script src="assets/highlight.pack.js"></script>
|
||||
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||
<script type="module" src="display_code.js"></script>
|
||||
<script type="module" src="counter.js"></script>
|
||||
</body>
|
||||
|
||||
+146
-82
@@ -122,14 +122,16 @@ function handleError(params) {
|
||||
}
|
||||
const node = "node" in params ? params.node : params.fiber.node;
|
||||
const fiber = "fiber" in params ? params.fiber : node.fiber;
|
||||
// resets the fibers on components if possible. This is important so that
|
||||
// new renderings can be properly included in the initial one, if any.
|
||||
let current = fiber;
|
||||
do {
|
||||
current.node.fiber = current;
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
fibersInError.set(fiber.root, error);
|
||||
if (fiber) {
|
||||
// resets the fibers on components if possible. This is important so that
|
||||
// new renderings can be properly included in the initial one, if any.
|
||||
let current = fiber;
|
||||
do {
|
||||
current.node.fiber = current;
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
fibersInError.set(fiber.root, error);
|
||||
}
|
||||
const handled = _handleError(node, error);
|
||||
if (!handled) {
|
||||
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
||||
@@ -175,11 +177,21 @@ function createAttrUpdater(attr) {
|
||||
}
|
||||
function attrsSetter(attrs) {
|
||||
if (isArray(attrs)) {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
if (attrs[0] === "class") {
|
||||
setClass.call(this, attrs[1]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let k in attrs) {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
if (k === "class") {
|
||||
setClass.call(this, attrs[k]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +203,12 @@ function attrsUpdater(attrs, oldAttrs) {
|
||||
if (val === oldAttrs[1]) {
|
||||
return;
|
||||
}
|
||||
setAttribute.call(this, name, val);
|
||||
if (name === "class") {
|
||||
updateClass.call(this, val, oldAttrs[1]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, name, val);
|
||||
}
|
||||
}
|
||||
else {
|
||||
removeAttribute.call(this, oldAttrs[0]);
|
||||
@@ -201,13 +218,23 @@ function attrsUpdater(attrs, oldAttrs) {
|
||||
else {
|
||||
for (let k in oldAttrs) {
|
||||
if (!(k in attrs)) {
|
||||
removeAttribute.call(this, k);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, "", oldAttrs[k]);
|
||||
}
|
||||
else {
|
||||
removeAttribute.call(this, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in attrs) {
|
||||
const val = attrs[k];
|
||||
if (val !== oldAttrs[k]) {
|
||||
setAttribute.call(this, k, val);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, val, oldAttrs[k]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,20 +313,13 @@ function updateClass(val, oldVal) {
|
||||
* @returns a batched version of the original callback
|
||||
*/
|
||||
function batched(callback) {
|
||||
let called = false;
|
||||
return async () => {
|
||||
// This await blocks all calls to the callback here, then releases them sequentially
|
||||
// in the next microtick. This line decides the granularity of the batch.
|
||||
await Promise.resolve();
|
||||
if (!called) {
|
||||
called = true;
|
||||
// wait for all calls in this microtick to fall through before resetting "called"
|
||||
// so that only the first call to the batched function calls the original callback.
|
||||
// Schedule this before calling the callback so that calls to the batched function
|
||||
// within the callback will proceed only after resetting called to false, and have
|
||||
// a chance to execute the callback again
|
||||
Promise.resolve().then(() => (called = false));
|
||||
callback();
|
||||
let scheduled = false;
|
||||
return async (...args) => {
|
||||
if (!scheduled) {
|
||||
scheduled = true;
|
||||
await Promise.resolve();
|
||||
scheduled = false;
|
||||
callback(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1632,8 +1652,7 @@ function cancelFibers(fibers) {
|
||||
let node = fiber.node;
|
||||
fiber.render = throwOnRender;
|
||||
if (node.status === 0 /* NEW */) {
|
||||
node.destroy();
|
||||
delete node.parent.children[node.parentKey];
|
||||
node.cancel();
|
||||
}
|
||||
node.fiber = null;
|
||||
if (fiber.bdom) {
|
||||
@@ -2360,6 +2379,9 @@ class ComponentNode {
|
||||
}
|
||||
}
|
||||
async render(deep) {
|
||||
if (this.status >= 2 /* CANCELLED */) {
|
||||
return;
|
||||
}
|
||||
let current = this.fiber;
|
||||
if (current && (current.root.locked || current.bdom === true)) {
|
||||
await Promise.resolve();
|
||||
@@ -2385,7 +2407,7 @@ class ComponentNode {
|
||||
this.fiber = fiber;
|
||||
this.app.scheduler.addFiber(fiber);
|
||||
await Promise.resolve();
|
||||
if (this.status === 2 /* DESTROYED */) {
|
||||
if (this.status >= 2 /* CANCELLED */) {
|
||||
return;
|
||||
}
|
||||
// We only want to actually render the component if the following two
|
||||
@@ -2403,6 +2425,18 @@ class ComponentNode {
|
||||
fiber.render();
|
||||
}
|
||||
}
|
||||
cancel() {
|
||||
this._cancel();
|
||||
delete this.parent.children[this.parentKey];
|
||||
this.app.scheduler.scheduleDestroy(this);
|
||||
}
|
||||
_cancel() {
|
||||
this.status = 2 /* CANCELLED */;
|
||||
const children = this.children;
|
||||
for (let childKey in children) {
|
||||
children[childKey]._cancel();
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
let shouldRemove = this.status === 1 /* MOUNTED */;
|
||||
this._destroy();
|
||||
@@ -2430,7 +2464,7 @@ class ComponentNode {
|
||||
this.app.handleError({ error: e, node: this });
|
||||
}
|
||||
}
|
||||
this.status = 2 /* DESTROYED */;
|
||||
this.status = 3 /* DESTROYED */;
|
||||
}
|
||||
async updateAndRender(props, parentFiber) {
|
||||
this.nextProps = props;
|
||||
@@ -2963,12 +2997,22 @@ function prepareList(collection) {
|
||||
keys = collection;
|
||||
values = collection;
|
||||
}
|
||||
else if (collection) {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
else if (collection instanceof Map) {
|
||||
keys = [...collection.keys()];
|
||||
values = [...collection.values()];
|
||||
}
|
||||
else if (collection && typeof collection === "object") {
|
||||
if (Symbol.iterator in collection) {
|
||||
keys = [...collection];
|
||||
values = keys;
|
||||
}
|
||||
else {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new OwlError("Invalid loop expression");
|
||||
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
|
||||
}
|
||||
const n = values.length;
|
||||
return [keys, values, n, new Array(n)];
|
||||
@@ -3221,7 +3265,7 @@ class TemplateSet {
|
||||
}
|
||||
callTemplate(owner, subTemplate, ctx, parent, key) {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3875,6 +3919,10 @@ class CodeGenerator {
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
translate(str) {
|
||||
const match = translationRE.exec(str);
|
||||
return match[1] + this.translateFn(match[2]) + match[3];
|
||||
}
|
||||
/**
|
||||
* @returns the newly created block name, if any
|
||||
*/
|
||||
@@ -3952,8 +4000,7 @@ class CodeGenerator {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
let value = ast.value;
|
||||
if (value && ctx.translate !== false) {
|
||||
const match = translationRE.exec(value);
|
||||
value = match[1] + this.translateFn(match[2]) + match[3];
|
||||
value = this.translate(value);
|
||||
}
|
||||
if (!ctx.inPreTag) {
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
@@ -4494,11 +4541,12 @@ class CodeGenerator {
|
||||
else {
|
||||
let value;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
||||
}
|
||||
else {
|
||||
value = `\`${ast.defaultValue}\``;
|
||||
value = `\`${defaultValue}\``;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -4791,10 +4839,10 @@ function parseNode(node, ctx) {
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
parseTSetNode(node, ctx) ||
|
||||
@@ -4879,10 +4927,10 @@ function parseDOMNode(node, ctx) {
|
||||
let model = null;
|
||||
for (let attr of nodeAttrsNames) {
|
||||
const value = node.getAttribute(attr);
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr === "t-on" || attr === "t-on-") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attr.slice(5)] = value;
|
||||
}
|
||||
@@ -4907,10 +4955,8 @@ function parseDOMNode(node, ctx) {
|
||||
const typeAttr = node.getAttribute("type");
|
||||
const isInput = tagName === "input";
|
||||
const isSelect = tagName === "select";
|
||||
const isTextarea = tagName === "textarea";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
@@ -4922,8 +4968,8 @@ function parseDOMNode(node, ctx) {
|
||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||
eventType,
|
||||
hasDynamicChildren: false,
|
||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||
shouldTrim: hasTrimMod,
|
||||
shouldNumberize: hasNumberMod,
|
||||
};
|
||||
if (isSelect) {
|
||||
// don't pollute the original ctx
|
||||
@@ -4986,9 +5032,6 @@ function parseTEscNode(node, ctx) {
|
||||
content: [tesc],
|
||||
};
|
||||
}
|
||||
if (ast.type === 11 /* TComponent */) {
|
||||
throw new OwlError("t-esc is not supported on Component nodes");
|
||||
}
|
||||
return tesc;
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -5430,19 +5473,21 @@ function normalizeTIf(el) {
|
||||
*
|
||||
* @param el the element containing the tree that should be normalized
|
||||
*/
|
||||
function normalizeTEsc(el) {
|
||||
const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new OwlError("Cannot have t-esc on a component that already has content");
|
||||
function normalizeTEscTOut(el) {
|
||||
for (const d of ["t-esc", "t-out"]) {
|
||||
const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new OwlError(`Cannot have ${d} on a component that already has content`);
|
||||
}
|
||||
const value = el.getAttribute(d);
|
||||
el.removeAttribute(d);
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute(d, value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
const value = el.getAttribute("t-esc");
|
||||
el.removeAttribute("t-esc");
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute("t-esc", value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -5453,7 +5498,7 @@ function normalizeTEsc(el) {
|
||||
*/
|
||||
function normalizeXML(el) {
|
||||
normalizeTIf(el);
|
||||
normalizeTEsc(el);
|
||||
normalizeTEscTOut(el);
|
||||
}
|
||||
/**
|
||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||
@@ -5506,7 +5551,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.1.1";
|
||||
const version = "2.2";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5516,11 +5561,18 @@ class Scheduler {
|
||||
this.tasks = new Set();
|
||||
this.frame = 0;
|
||||
this.delayedRenders = [];
|
||||
this.cancelledNodes = new Set();
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
}
|
||||
addFiber(fiber) {
|
||||
this.tasks.add(fiber.root);
|
||||
}
|
||||
scheduleDestroy(node) {
|
||||
this.cancelledNodes.add(node);
|
||||
if (this.frame === 0) {
|
||||
this.frame = this.requestAnimationFrame(() => this.processTasks());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Process all current tasks. This only applies to the fibers that are ready.
|
||||
* Other tasks are left unchanged.
|
||||
@@ -5530,21 +5582,28 @@ class Scheduler {
|
||||
let renders = this.delayedRenders;
|
||||
this.delayedRenders = [];
|
||||
for (let f of renders) {
|
||||
if (f.root && f.node.status !== 2 /* DESTROYED */ && f.node.fiber === f) {
|
||||
if (f.root && f.node.status !== 3 /* DESTROYED */ && f.node.fiber === f) {
|
||||
f.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.frame === 0) {
|
||||
this.frame = this.requestAnimationFrame(() => {
|
||||
this.frame = 0;
|
||||
this.tasks.forEach((fiber) => this.processFiber(fiber));
|
||||
for (let task of this.tasks) {
|
||||
if (task.node.status === 2 /* DESTROYED */) {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.frame = this.requestAnimationFrame(() => this.processTasks());
|
||||
}
|
||||
}
|
||||
processTasks() {
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
}
|
||||
this.cancelledNodes.clear();
|
||||
for (let task of this.tasks) {
|
||||
this.processFiber(task);
|
||||
}
|
||||
for (let task of this.tasks) {
|
||||
if (task.node.status === 3 /* DESTROYED */) {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
processFiber(fiber) {
|
||||
@@ -5557,7 +5616,7 @@ class Scheduler {
|
||||
this.tasks.delete(fiber);
|
||||
return;
|
||||
}
|
||||
if (fiber.node.status === 2 /* DESTROYED */) {
|
||||
if (fiber.node.status === 3 /* DESTROYED */) {
|
||||
this.tasks.delete(fiber);
|
||||
return;
|
||||
}
|
||||
@@ -5585,6 +5644,8 @@ window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = {
|
||||
apps: new Set(),
|
||||
Fiber: Fiber,
|
||||
RootFiber: RootFiber,
|
||||
toRaw: toRaw,
|
||||
reactive: reactive,
|
||||
});
|
||||
class App extends TemplateSet {
|
||||
constructor(Root, config = {}) {
|
||||
@@ -5779,9 +5840,11 @@ function status(component) {
|
||||
switch (component.__owl__.status) {
|
||||
case 0 /* NEW */:
|
||||
return "new";
|
||||
case 2 /* CANCELLED */:
|
||||
return "cancelled";
|
||||
case 1 /* MOUNTED */:
|
||||
return "mounted";
|
||||
case 2 /* DESTROYED */:
|
||||
case 3 /* DESTROYED */:
|
||||
return "destroyed";
|
||||
}
|
||||
}
|
||||
@@ -5837,8 +5900,9 @@ function useChildSubEnv(envExtension) {
|
||||
* will run a cleanup function before patching and before unmounting the
|
||||
* the component.
|
||||
*
|
||||
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* @template T
|
||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
@@ -5920,6 +5984,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
|
||||
|
||||
__info__.date = '2023-04-24T14:29:32.376Z';
|
||||
__info__.hash = 'f9d810a';
|
||||
__info__.date = '2023-07-19T13:22:24.480Z';
|
||||
__info__.hash = '2e07799';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.1.2",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.1.2",
|
||||
"version": "2.2.1",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -447,6 +447,11 @@ export class CodeGenerator {
|
||||
.join("");
|
||||
}
|
||||
|
||||
translate(str: string): string {
|
||||
const match = translationRE.exec(str) as any;
|
||||
return match[1] + this.translateFn(match[2]) + match[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the newly created block name, if any
|
||||
*/
|
||||
@@ -527,8 +532,7 @@ export class CodeGenerator {
|
||||
|
||||
let value = ast.value;
|
||||
if (value && ctx.translate !== false) {
|
||||
const match = translationRE.exec(value) as any;
|
||||
value = match[1] + this.translateFn(match[2]) + match[3];
|
||||
value = this.translate(value);
|
||||
}
|
||||
if (!ctx.inPreTag) {
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
@@ -1095,10 +1099,11 @@ export class CodeGenerator {
|
||||
} else {
|
||||
let value: string;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
||||
} else {
|
||||
value = `\`${ast.defaultValue}\``;
|
||||
value = `\`${defaultValue}\``;
|
||||
}
|
||||
} else {
|
||||
value = expr;
|
||||
|
||||
+24
-27
@@ -235,10 +235,10 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
parseTSetNode(node, ctx) ||
|
||||
@@ -336,10 +336,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
|
||||
for (let attr of nodeAttrsNames) {
|
||||
const value = node.getAttribute(attr)!;
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr === "t-on" || attr === "t-on-") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attr.slice(5)] = value;
|
||||
} else if (attr.startsWith("t-model")) {
|
||||
@@ -365,10 +365,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
const typeAttr = node.getAttribute("type");
|
||||
const isInput = tagName === "input";
|
||||
const isSelect = tagName === "select";
|
||||
const isTextarea = tagName === "textarea";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
@@ -381,8 +379,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||
eventType,
|
||||
hasDynamicChildren: false,
|
||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||
shouldTrim: hasTrimMod,
|
||||
shouldNumberize: hasNumberMod,
|
||||
};
|
||||
if (isSelect) {
|
||||
// don't pollute the original ctx
|
||||
@@ -446,9 +444,6 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
content: [tesc],
|
||||
};
|
||||
}
|
||||
if (ast.type === ASTType.TComponent) {
|
||||
throw new OwlError("t-esc is not supported on Component nodes");
|
||||
}
|
||||
return tesc;
|
||||
}
|
||||
|
||||
@@ -943,21 +938,23 @@ function normalizeTIf(el: Element) {
|
||||
*
|
||||
* @param el the element containing the tree that should be normalized
|
||||
*/
|
||||
function normalizeTEsc(el: Element) {
|
||||
const elements = [...el.querySelectorAll("[t-esc]")].filter(
|
||||
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
||||
);
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new OwlError("Cannot have t-esc on a component that already has content");
|
||||
function normalizeTEscTOut(el: Element) {
|
||||
for (const d of ["t-esc", "t-out"]) {
|
||||
const elements = [...el.querySelectorAll(`[${d}]`)].filter(
|
||||
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
||||
);
|
||||
for (const el of elements) {
|
||||
if (el.childNodes.length) {
|
||||
throw new OwlError(`Cannot have ${d} on a component that already has content`);
|
||||
}
|
||||
const value = el.getAttribute(d);
|
||||
el.removeAttribute(d);
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute(d, value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
const value = el.getAttribute("t-esc");
|
||||
el.removeAttribute("t-esc");
|
||||
const t = el.ownerDocument.createElement("t");
|
||||
if (value != null) {
|
||||
t.setAttribute("t-esc", value);
|
||||
}
|
||||
el.appendChild(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -969,7 +966,7 @@ function normalizeTEsc(el: Element) {
|
||||
*/
|
||||
function normalizeXML(el: Element) {
|
||||
normalizeTIf(el);
|
||||
normalizeTEsc(el);
|
||||
normalizeTEscTOut(el);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Scheduler } from "./scheduler";
|
||||
import { validateProps } from "./template_helpers";
|
||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||
import { validateTarget } from "./utils";
|
||||
import { toRaw, reactive } from "./reactivity";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
@@ -39,6 +40,8 @@ declare global {
|
||||
apps: Set<App>;
|
||||
Fiber: typeof Fiber;
|
||||
RootFiber: typeof RootFiber;
|
||||
toRaw: typeof toRaw;
|
||||
reactive: typeof reactive;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -47,6 +50,8 @@ window.__OWL_DEVTOOLS__ ||= {
|
||||
apps: new Set<App>(),
|
||||
Fiber: Fiber,
|
||||
RootFiber: RootFiber,
|
||||
toRaw: toRaw,
|
||||
reactive: reactive,
|
||||
};
|
||||
|
||||
export class App<
|
||||
|
||||
@@ -36,10 +36,18 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
||||
|
||||
export function attrsSetter(this: HTMLElement, attrs: any) {
|
||||
if (isArray(attrs)) {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
if (attrs[0] === "class") {
|
||||
setClass.call(this, attrs[1]);
|
||||
} else {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
}
|
||||
} else {
|
||||
for (let k in attrs) {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
if (k === "class") {
|
||||
setClass.call(this, attrs[k]);
|
||||
} else {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +60,11 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
if (val === oldAttrs[1]) {
|
||||
return;
|
||||
}
|
||||
setAttribute.call(this, name, val);
|
||||
if (name === "class") {
|
||||
updateClass.call(this, val, oldAttrs[1]);
|
||||
} else {
|
||||
setAttribute.call(this, name, val);
|
||||
}
|
||||
} else {
|
||||
removeAttribute.call(this, oldAttrs[0]);
|
||||
setAttribute.call(this, name, val);
|
||||
@@ -60,13 +72,21 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
} else {
|
||||
for (let k in oldAttrs) {
|
||||
if (!(k in attrs)) {
|
||||
removeAttribute.call(this, k);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, "", oldAttrs[k]);
|
||||
} else {
|
||||
removeAttribute.call(this, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in attrs) {
|
||||
const val = attrs[k];
|
||||
if (val !== oldAttrs[k]) {
|
||||
setAttribute.call(this, k, val);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, val, oldAttrs[k]);
|
||||
} else {
|
||||
setAttribute.call(this, k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,9 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
}
|
||||
|
||||
async render(deep: boolean) {
|
||||
if (this.status >= STATUS.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
let current = this.fiber;
|
||||
if (current && (current.root!.locked || (current as any).bdom === true)) {
|
||||
await Promise.resolve();
|
||||
@@ -171,7 +174,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
|
||||
this.app.scheduler.addFiber(fiber);
|
||||
await Promise.resolve();
|
||||
if (this.status === STATUS.DESTROYED) {
|
||||
if (this.status >= STATUS.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
// We only want to actually render the component if the following two
|
||||
@@ -190,6 +193,20 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._cancel();
|
||||
delete this.parent!.children[this.parentKey!];
|
||||
this.app.scheduler.scheduleDestroy(this);
|
||||
}
|
||||
|
||||
_cancel() {
|
||||
this.status = STATUS.CANCELLED;
|
||||
const children = this.children;
|
||||
for (let childKey in children) {
|
||||
children[childKey]._cancel();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
let shouldRemove = this.status === STATUS.MOUNTED;
|
||||
this._destroy();
|
||||
|
||||
@@ -51,17 +51,19 @@ export function handleError(params: ErrorParams) {
|
||||
);
|
||||
}
|
||||
const node = "node" in params ? params.node : params.fiber.node;
|
||||
const fiber = "fiber" in params ? params.fiber : node.fiber!;
|
||||
const fiber = "fiber" in params ? params.fiber : node.fiber;
|
||||
|
||||
// resets the fibers on components if possible. This is important so that
|
||||
// new renderings can be properly included in the initial one, if any.
|
||||
let current: Fiber | null = fiber;
|
||||
do {
|
||||
current.node.fiber = current;
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
if (fiber) {
|
||||
// resets the fibers on components if possible. This is important so that
|
||||
// new renderings can be properly included in the initial one, if any.
|
||||
let current: Fiber | null = fiber;
|
||||
do {
|
||||
current.node.fiber = current;
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
|
||||
fibersInError.set(fiber.root!, error);
|
||||
fibersInError.set(fiber.root!, error);
|
||||
}
|
||||
|
||||
const handled = _handleError(node, error);
|
||||
if (!handled) {
|
||||
|
||||
@@ -55,8 +55,7 @@ function cancelFibers(fibers: Fiber[]): number {
|
||||
let node = fiber.node;
|
||||
fiber.render = throwOnRender;
|
||||
if (node.status === STATUS.NEW) {
|
||||
node.destroy();
|
||||
delete node.parent!.children[node.parentKey!];
|
||||
node.cancel();
|
||||
}
|
||||
node.fiber = null;
|
||||
if (fiber.bdom) {
|
||||
|
||||
+13
-6
@@ -59,28 +59,35 @@ export function useChildSubEnv(envExtension: Env) {
|
||||
// useEffect
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type EffectDeps<T extends any[]> = T | (T extends [...infer H, never] ? EffectDeps<H> : never);
|
||||
|
||||
/**
|
||||
* @param {...any} dependencies the dependencies computed by computeDependencies
|
||||
* @template T
|
||||
* @param {...T} dependencies the dependencies computed by computeDependencies
|
||||
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
||||
* effects of the effect callback.
|
||||
*/
|
||||
type Effect = (...dependencies: any[]) => void | (() => void);
|
||||
type Effect<T extends [...T]> = (...dependencies: EffectDeps<T>) => void | (() => void);
|
||||
|
||||
/**
|
||||
* This hook will run a callback when a component is mounted and patched, and
|
||||
* will run a cleanup function before patching and before unmounting the
|
||||
* the component.
|
||||
*
|
||||
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* @template T
|
||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||||
*/
|
||||
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
|
||||
export function useEffect<T extends [...T]>(
|
||||
effect: Effect<T>,
|
||||
computeDependencies: () => T = () => [NaN] as never
|
||||
) {
|
||||
let cleanup: (() => void) | void;
|
||||
let dependencies: any[];
|
||||
let dependencies: T;
|
||||
onMounted(() => {
|
||||
dependencies = computeDependencies();
|
||||
cleanup = effect(...dependencies);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentNode } from "./component_node";
|
||||
import { fibersInError } from "./error_handling";
|
||||
import { Fiber, RootFiber } from "./fibers";
|
||||
import { STATUS } from "./status";
|
||||
@@ -14,6 +15,7 @@ export class Scheduler {
|
||||
requestAnimationFrame: Window["requestAnimationFrame"];
|
||||
frame: number = 0;
|
||||
delayedRenders: Fiber[] = [];
|
||||
cancelledNodes: Set<ComponentNode> = new Set();
|
||||
|
||||
constructor() {
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
@@ -23,6 +25,13 @@ export class Scheduler {
|
||||
this.tasks.add(fiber.root!);
|
||||
}
|
||||
|
||||
scheduleDestroy(node: ComponentNode) {
|
||||
this.cancelledNodes.add(node);
|
||||
if (this.frame === 0) {
|
||||
this.frame = this.requestAnimationFrame(() => this.processTasks());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all current tasks. This only applies to the fibers that are ready.
|
||||
* Other tasks are left unchanged.
|
||||
@@ -39,15 +48,23 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
if (this.frame === 0) {
|
||||
this.frame = this.requestAnimationFrame(() => {
|
||||
this.frame = 0;
|
||||
this.tasks.forEach((fiber) => this.processFiber(fiber));
|
||||
for (let task of this.tasks) {
|
||||
if (task.node.status === STATUS.DESTROYED) {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.frame = this.requestAnimationFrame(() => this.processTasks());
|
||||
}
|
||||
}
|
||||
|
||||
processTasks() {
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
}
|
||||
this.cancelledNodes.clear();
|
||||
for (let task of this.tasks) {
|
||||
this.processFiber(task);
|
||||
}
|
||||
for (let task of this.tasks) {
|
||||
if (task.node.status === STATUS.DESTROYED) {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,20 @@ import type { Component } from "./component";
|
||||
export const enum STATUS {
|
||||
NEW,
|
||||
MOUNTED, // is ready, and in DOM. It has a valid el
|
||||
// component has been created, but has been replaced by a newer component before being mounted
|
||||
// it is cancelled until the next animation frame where it will be destroyed
|
||||
CANCELLED,
|
||||
DESTROYED,
|
||||
}
|
||||
|
||||
type STATUS_DESCR = "new" | "mounted" | "destroyed";
|
||||
type STATUS_DESCR = "new" | "mounted" | "cancelled" | "destroyed";
|
||||
|
||||
export function status(component: Component): STATUS_DESCR {
|
||||
switch (component.__owl__.status) {
|
||||
case STATUS.NEW:
|
||||
return "new";
|
||||
case STATUS.CANCELLED:
|
||||
return "cancelled";
|
||||
case STATUS.MOUNTED:
|
||||
return "mounted";
|
||||
case STATUS.DESTROYED:
|
||||
|
||||
@@ -60,18 +60,26 @@ function withKey(elem: any, k: string) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||
let keys: any[];
|
||||
let values: any[];
|
||||
function prepareList(collection: unknown): [unknown[], unknown[], number, undefined[]] {
|
||||
let keys: unknown[];
|
||||
let values: unknown[];
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
keys = collection;
|
||||
values = collection;
|
||||
} else if (collection) {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
} else if (collection instanceof Map) {
|
||||
keys = [...collection.keys()];
|
||||
values = [...collection.values()];
|
||||
} else if (collection && typeof collection === "object") {
|
||||
if (Symbol.iterator in collection) {
|
||||
keys = [...(<Iterable<unknown>>collection)];
|
||||
values = keys;
|
||||
} else {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
}
|
||||
} else {
|
||||
throw new OwlError("Invalid loop expression");
|
||||
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
|
||||
}
|
||||
const n = values.length;
|
||||
return [keys, values, n, new Array(n)];
|
||||
|
||||
@@ -125,7 +125,7 @@ export class TemplateSet {
|
||||
|
||||
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key));
|
||||
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-14
@@ -9,20 +9,13 @@ export type Callback = () => void;
|
||||
* @returns a batched version of the original callback
|
||||
*/
|
||||
export function batched(callback: Callback): Callback {
|
||||
let called = false;
|
||||
return async () => {
|
||||
// This await blocks all calls to the callback here, then releases them sequentially
|
||||
// in the next microtick. This line decides the granularity of the batch.
|
||||
await Promise.resolve();
|
||||
if (!called) {
|
||||
called = true;
|
||||
// wait for all calls in this microtick to fall through before resetting "called"
|
||||
// so that only the first call to the batched function calls the original callback.
|
||||
// Schedule this before calling the callback so that calls to the batched function
|
||||
// within the callback will proceed only after resetting called to false, and have
|
||||
// a chance to execute the callback again
|
||||
Promise.resolve().then(() => (called = false));
|
||||
callback();
|
||||
let scheduled = false;
|
||||
return async (...args) => {
|
||||
if (!scheduled) {
|
||||
scheduled = true;
|
||||
await Promise.resolve();
|
||||
scheduled = false;
|
||||
callback(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.1.2";
|
||||
export const version = "2.2.1";
|
||||
|
||||
@@ -145,3 +145,34 @@ test("class attribute (with a preexisting value", async () => {
|
||||
patch(tree, block([""]));
|
||||
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
|
||||
});
|
||||
|
||||
test("block-class attributes with preexisting class attribute", async () => {
|
||||
const block = createBlock('<div block-attributes="0" class="owl"></div>');
|
||||
const tree = block([{ class: "eagle" }]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
|
||||
|
||||
patch(tree, block([{ class: "falcon" }]));
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
|
||||
|
||||
patch(tree, block([{}]));
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
|
||||
});
|
||||
|
||||
test("block-class attributes (array syntax) with preexisting class attribute", async () => {
|
||||
const block = createBlock('<div block-attributes="0" class="owl"></div>');
|
||||
const tree = block([["class", "eagle"]]);
|
||||
|
||||
mount(tree, fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
|
||||
|
||||
patch(tree, block([["class", "falcon"]]));
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
|
||||
|
||||
patch(tree, block([["class", ""]]));
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
|
||||
|
||||
patch(tree, block([["class", "buzzard"]]));
|
||||
expect(fixture.innerHTML).toBe(`<div class="owl buzzard"></div>`);
|
||||
});
|
||||
|
||||
@@ -707,6 +707,123 @@ exports[`attributes updating classes (with obj notation) 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {class:'a'};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\" class=\\"c\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {class:'a'};
|
||||
let attr2 = {'b':true};
|
||||
return block1([attr1, attr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\" block-attribute-1=\\"class\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {class:'a'};
|
||||
let attr2 = {'b':true};
|
||||
return block1([attr1, attr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"c\\" block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {class:'a'};
|
||||
let attr2 = {'b':true};
|
||||
return block1([attr1, attr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\" block-attributes=\\"1\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {'b':true};
|
||||
let attr2 = {class:'a'};
|
||||
return block1([attr1, attr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 6`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {'b':true};
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 7`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = ('b');
|
||||
return block1([attr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various combinations of class, t-att-class, and t-att 8`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let attr1 = {class:'a'};
|
||||
let attr2 = {'b':true};
|
||||
return block1([attr1, attr2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`attributes various escapes 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -77,6 +77,62 @@ exports[`t-foreach iterate on items 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-foreach iterate, Map param 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);;
|
||||
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||
ctx[\`item\`] = v_block1[i1];
|
||||
ctx[\`item_index\`] = i1;
|
||||
ctx[\`item_value\`] = k_block1[i1];
|
||||
const key1 = ctx['item_index'];
|
||||
const b3 = text(\` [\`);
|
||||
const b4 = text(ctx['item_index']);
|
||||
const b5 = text(\`: \`);
|
||||
const b6 = text(ctx['item']);
|
||||
const b7 = text(\` \`);
|
||||
const b8 = text(ctx['item_value']);
|
||||
const b9 = text(\`] \`);
|
||||
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-foreach iterate, Set param 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);;
|
||||
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||
ctx[\`item\`] = v_block1[i1];
|
||||
ctx[\`item_index\`] = i1;
|
||||
ctx[\`item_value\`] = k_block1[i1];
|
||||
const key1 = ctx['item_index'];
|
||||
const b3 = text(\` [\`);
|
||||
const b4 = text(ctx['item_index']);
|
||||
const b5 = text(\`: \`);
|
||||
const b6 = text(ctx['item']);
|
||||
const b7 = text(\` \`);
|
||||
const b8 = text(ctx['item_value']);
|
||||
const b9 = text(\`] \`);
|
||||
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-foreach iterate, dict param 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -108,6 +164,62 @@ exports[`t-foreach iterate, dict param 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-foreach iterate, generator param 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['gen']());;
|
||||
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||
ctx[\`item\`] = v_block1[i1];
|
||||
ctx[\`item_index\`] = i1;
|
||||
ctx[\`item_value\`] = k_block1[i1];
|
||||
const key1 = ctx['item_index'];
|
||||
const b3 = text(\` [\`);
|
||||
const b4 = text(ctx['item_index']);
|
||||
const b5 = text(\`: \`);
|
||||
const b6 = text(ctx['item']);
|
||||
const b7 = text(\` \`);
|
||||
const b8 = text(ctx['item_value']);
|
||||
const b9 = text(\`] \`);
|
||||
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-foreach iterate, iterable param 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['map'].values());;
|
||||
for (let i1 = 0; i1 < l_block1; i1++) {
|
||||
ctx[\`item\`] = v_block1[i1];
|
||||
ctx[\`item_index\`] = i1;
|
||||
ctx[\`item_value\`] = k_block1[i1];
|
||||
const key1 = ctx['item_index'];
|
||||
const b3 = text(\` [\`);
|
||||
const b4 = text(ctx['item_index']);
|
||||
const b5 = text(\`: \`);
|
||||
const b6 = text(ctx['item']);
|
||||
const b7 = text(\` \`);
|
||||
const b8 = text(ctx['item_value']);
|
||||
const b9 = text(\`] \`);
|
||||
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
|
||||
}
|
||||
return list(c_block1);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-foreach iterate, position 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,79 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`translation support body of t-sets are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"label\\", \`translated\`);
|
||||
return text(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support body of t-sets inside translation=off are not translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"label\\", \`untranslated\`);
|
||||
return text(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support body of t-sets with html content are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div>translated</div>\`);
|
||||
|
||||
function value1(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
|
||||
return safeOutput(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support body of t-sets with text and html content are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
|
||||
|
||||
let block3 = createBlock(\`<div>translated</div>\`);
|
||||
|
||||
function value1(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\` translated \`);
|
||||
const b3 = block3();
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
|
||||
return safeOutput(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support can set and remove translatable attributes 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -52,6 +126,21 @@ exports[`translation support some attributes are translated 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support t-set and falsy t-value: t-body are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"label\\", withDefault(false, \`translated\`));
|
||||
return text(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -371,4 +371,33 @@ describe("attributes", () => {
|
||||
// not sure about this. maybe we want to remove the attribute?
|
||||
expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>');
|
||||
});
|
||||
|
||||
test("various combinations of class, t-att-class, and t-att", () => {
|
||||
const template1 = `<div t-att="{ class: 'a' }" class="c">content</div>`;
|
||||
expect(renderToString(template1)).toBe('<div class="c a">content</div>');
|
||||
|
||||
const template2 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}" class="c">content</div>`;
|
||||
expect(renderToString(template2)).toBe('<div class="c a b">content</div>');
|
||||
|
||||
const template3 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
|
||||
expect(renderToString(template3)).toBe('<div class="c a b">content</div>');
|
||||
|
||||
const template4 = `<div class="c" t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
|
||||
expect(renderToString(template4)).toBe('<div class="c a b">content</div>');
|
||||
|
||||
const template5 = `<div class="c" t-att-class="{'b': true}" t-att="{ class: 'a' }">content</div>`;
|
||||
expect(renderToString(template5)).toBe('<div class="c b a">content</div>');
|
||||
|
||||
const template6 = `<div class="c" t-att-class="{'b': true}">content</div>`;
|
||||
expect(renderToString(template6)).toBe('<div class="c b">content</div>');
|
||||
|
||||
const template7 = `<div class="c" t-attf-class="{{'b'}}">content</div>`;
|
||||
expect(renderToString(template7)).toBe('<div class="c b">content</div>');
|
||||
|
||||
const template8 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
|
||||
expect(renderToString(template8)).toBe('<div class="c a b">content</div>');
|
||||
|
||||
const template9 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
|
||||
expect(renderToString(template9)).toBe('<div class="a b">content</div>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1147,6 +1147,24 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("t-onclick without dash", async () => {
|
||||
expect(() => parse(`<button t-onclick="add">Click</button>`)).toThrowError(
|
||||
"Unknown QWeb directive: 't-onclick'"
|
||||
);
|
||||
});
|
||||
|
||||
test("t-on without event", async () => {
|
||||
expect(() => parse(`<button t-on="add">Click</button>`)).toThrowError(
|
||||
"Missing event name with t-on directive"
|
||||
);
|
||||
});
|
||||
|
||||
test("t-on- without event", async () => {
|
||||
expect(() => parse(`<button t-on-="add">Click</button>`)).toThrowError(
|
||||
"Missing event name with t-on directive"
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-model
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1275,6 +1293,12 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("component with event handler", async () => {
|
||||
expect(() => parse(`<MyComponent t-onclick="someMethod"/>`)).toThrowError(
|
||||
"unsupported directive on Component: t-onclick"
|
||||
);
|
||||
});
|
||||
|
||||
test("component with t-ref", async () => {
|
||||
expect(() => parse(`<MyComponent t-ref="something"/>`)).toThrow(
|
||||
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop."
|
||||
@@ -1545,6 +1569,12 @@ describe("qweb parser", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("component with t-out", async () => {
|
||||
expect(parse(`<MyComponent t-out="someValue"/>`)).toEqual(
|
||||
parse(`<MyComponent><t t-out="someValue"/></MyComponent>`)
|
||||
);
|
||||
});
|
||||
|
||||
test("component with t-esc and content", async () => {
|
||||
expect(() => parse(`<MyComponent t-esc="someValue">Some content</MyComponent>`)).toThrow(
|
||||
"Cannot have t-esc on a component that already has content"
|
||||
@@ -1967,8 +1997,8 @@ describe("qweb parser", () => {
|
||||
baseExpr: "state",
|
||||
expr: "'stuff'",
|
||||
eventType: "click",
|
||||
shouldNumberize: false,
|
||||
shouldTrim: false,
|
||||
shouldNumberize: true,
|
||||
shouldTrim: true,
|
||||
targetAttr: "value",
|
||||
hasDynamicChildren: false,
|
||||
specialInitTargetAttr: "checked",
|
||||
|
||||
@@ -105,6 +105,64 @@ describe("t-foreach", () => {
|
||||
expect(renderToString(template, context)).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, Map param", () => {
|
||||
const template = `
|
||||
<t t-foreach="value" t-as="item" t-key="item_index">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>`;
|
||||
const expected = ` [0: 1 a] [1: 2 b] [2: 3 c] `;
|
||||
const context = {
|
||||
value: new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]),
|
||||
};
|
||||
expect(renderToString(template, context)).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, Set param", () => {
|
||||
const template = `
|
||||
<t t-foreach="value" t-as="item" t-key="item_index">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>`;
|
||||
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
|
||||
const context = { value: new Set([1, 2, 3]) };
|
||||
expect(renderToString(template, context)).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, iterable param", () => {
|
||||
const template = `
|
||||
<t t-foreach="map.values()" t-as="item" t-key="item_index">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>`;
|
||||
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
|
||||
const context = {
|
||||
map: new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]),
|
||||
};
|
||||
expect(renderToString(template, context)).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, generator param", () => {
|
||||
const template = `
|
||||
<t t-foreach="gen()" t-as="item" t-key="item_index">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>`;
|
||||
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
|
||||
const context = {
|
||||
*gen() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
},
|
||||
};
|
||||
expect(renderToString(template, context)).toBe(expected);
|
||||
});
|
||||
|
||||
test("does not pollute the rendering context", () => {
|
||||
const template = `
|
||||
<div>
|
||||
@@ -193,7 +251,9 @@ describe("t-foreach", () => {
|
||||
|
||||
test("throws error if invalid loop expression", () => {
|
||||
const test = `<div><t t-foreach="abc" t-as="item" t-key="item"><span t-key="item_index"/></t></div>`;
|
||||
expect(() => renderToString(test)).toThrow("Invalid loop expression");
|
||||
expect(() => renderToString(test)).toThrow(
|
||||
'Invalid loop expression: "undefined" is not iterable'
|
||||
);
|
||||
});
|
||||
|
||||
test("t-foreach with t-if inside", () => {
|
||||
|
||||
@@ -100,4 +100,74 @@ describe("translation support", () => {
|
||||
expect(translateFn).toHaveBeenCalledWith("some word");
|
||||
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
||||
});
|
||||
|
||||
test("body of t-sets are translated", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-set="label">untranslated</t>
|
||||
<t t-esc="label"/>`;
|
||||
}
|
||||
|
||||
const translateFn = () => "translated";
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("translated");
|
||||
});
|
||||
|
||||
test("body of t-sets inside translation=off are not translated", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-translation="off">
|
||||
<t t-set="label">untranslated</t>
|
||||
<t t-esc="label"/>
|
||||
</t>`;
|
||||
}
|
||||
|
||||
const translateFn = () => "translated";
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("untranslated");
|
||||
});
|
||||
|
||||
test("body of t-sets with html content are translated", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-set="label"><div>untranslated</div></t>
|
||||
<t t-out="label"/>`;
|
||||
}
|
||||
|
||||
const translateFn = () => "translated";
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("<div>translated</div>");
|
||||
});
|
||||
|
||||
test("body of t-sets with text and html content are translated", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-set="label">
|
||||
some text
|
||||
<div>untranslated</div>
|
||||
</t>
|
||||
<t t-out="label"/>`;
|
||||
}
|
||||
|
||||
const translateFn = () => "translated";
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe(" translated <div>translated</div>");
|
||||
});
|
||||
|
||||
test("t-set and falsy t-value: t-body are translated", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-set="label" t-value="false">untranslated</t>
|
||||
<t t-esc="label"/>`;
|
||||
}
|
||||
|
||||
const translateFn = () => "translated";
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("translated");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,6 +212,73 @@ exports[`changing state before first render does not trigger a render 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`component destroyed just after render 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`B\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`component destroyed just after render 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`B\`);
|
||||
const b3 = text(ctx['state'].value);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`components are not destroyed between animation frame 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`B\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2,b3;
|
||||
b2 = text(\`A\`);
|
||||
if (ctx['state'].flag) {
|
||||
b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`components are not destroyed between animation frame 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`C\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`B\`);
|
||||
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`components are not destroyed between animation frame 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`C\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`concurrent renderings scenario 1 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -42,6 +42,56 @@ exports[`t-call dynamic t-call 3`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call dynamic t-call with same sub component 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const call = app.callTemplate.bind(app);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(ctx['current'].template);
|
||||
const template1 = (ctx['current'].template);
|
||||
const b3 = call(this, template1, ctx, node, key + \`__1\`);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call dynamic t-call with same sub component 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call dynamic t-call with same sub component 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`child\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call dynamic t-call with same sub component 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call dynamic t-call: key is propagated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -468,6 +468,36 @@ exports[`t-model directive t-model on select with static options 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model with dynamic number values on select options in foreach 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { toNumber, prepareList, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<select block-handler-0=\\"change\\"><block-child-0/></select>\`);
|
||||
let block3 = createBlock(\`<option block-attribute-0=\\"value\\" block-attribute-1=\\"selected\\"><block-text-2/></option>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const bExpr1 = ctx['state'];
|
||||
const expr1 = 'value';
|
||||
const bValue1 = bExpr1[expr1];
|
||||
let hdlr1 = [(ev) => { bExpr1[expr1] = toNumber(ev.target.value); }];
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['state'].options);;
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`o\`] = v_block2[i1];
|
||||
const key1 = ctx['o'].value;
|
||||
let attr1 = ctx['o'].value;
|
||||
let attr2 = bValue1 === ctx['o'].value;
|
||||
let txt1 = ctx['o'].value;
|
||||
c_block2[i1] = withKey(block3([attr1, attr2, txt1]), key1);
|
||||
}
|
||||
const b2 = list(c_block2);
|
||||
return block1([hdlr1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model with dynamic values on select options -- 2 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -115,13 +115,7 @@ test("destroying/recreating a subwidget with different props (if start is not ov
|
||||
await nextMicroTick();
|
||||
expect(n).toBe(2);
|
||||
|
||||
expect([
|
||||
"Child:willDestroy",
|
||||
"W:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"W:rendered",
|
||||
]).toBeLogged();
|
||||
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
@@ -130,6 +124,7 @@ test("destroying/recreating a subwidget with different props (if start is not ov
|
||||
expect([
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willDestroy",
|
||||
"W:willPatch",
|
||||
"Child:mounted",
|
||||
"W:patched",
|
||||
@@ -178,13 +173,13 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willPatch",
|
||||
"Child:mounted",
|
||||
"Parent:patched",
|
||||
@@ -251,13 +246,13 @@ test("creating two async components, scenario 1", async () => {
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect([
|
||||
"ChildA:willDestroy",
|
||||
"Parent:willRender",
|
||||
"ChildA:setup",
|
||||
"ChildA:willStart",
|
||||
"ChildB:setup",
|
||||
"ChildB:willStart",
|
||||
"Parent:rendered",
|
||||
"ChildA:willDestroy",
|
||||
]).toBeLogged();
|
||||
|
||||
defB.resolve();
|
||||
@@ -703,13 +698,13 @@ test("rendering component again in next microtick", async () => {
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willPatch",
|
||||
"Child:mounted",
|
||||
"Parent:patched",
|
||||
@@ -1732,9 +1727,9 @@ test("concurrent renderings scenario 10", async () => {
|
||||
expect(fixture.innerHTML).toBe("<div><p></p></div>");
|
||||
expect([
|
||||
"ComponentA:willRender",
|
||||
"ComponentC:willDestroy",
|
||||
"ComponentB:willUpdateProps",
|
||||
"ComponentA:rendered",
|
||||
"ComponentC:willDestroy",
|
||||
]).toBeLogged();
|
||||
|
||||
defB.resolve();
|
||||
@@ -2282,7 +2277,6 @@ test("concurrent renderings scenario 16", async () => {
|
||||
"D:setup",
|
||||
"D:willStart",
|
||||
"C:rendered",
|
||||
"D:willDestroy",
|
||||
"B:willRender",
|
||||
"C:willUpdateProps",
|
||||
"B:rendered",
|
||||
@@ -2290,6 +2284,7 @@ test("concurrent renderings scenario 16", async () => {
|
||||
"D:setup",
|
||||
"D:willStart",
|
||||
"C:rendered",
|
||||
"D:willDestroy",
|
||||
]).toBeLogged();
|
||||
|
||||
// at this point, C rendering is still pending, and nothing should have been
|
||||
@@ -2997,11 +2992,11 @@ test("t-key on dom node having a component", async () => {
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
expect([
|
||||
"Child (2):willDestroy",
|
||||
"Child (3):setup",
|
||||
"Child (3):willStart",
|
||||
"Child (3):willRender",
|
||||
"Child (3):rendered",
|
||||
"Child (2):willDestroy",
|
||||
"Child (1):willUnmount",
|
||||
"Child (1):willDestroy",
|
||||
"Child (3):mounted",
|
||||
@@ -3055,11 +3050,11 @@ test("t-key on dynamic async component (toggler is never patched)", async () =>
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
expect([
|
||||
"Child (2):willDestroy",
|
||||
"Child (3):setup",
|
||||
"Child (3):willStart",
|
||||
"Child (3):willRender",
|
||||
"Child (3):rendered",
|
||||
"Child (2):willDestroy",
|
||||
"Child (1):willUnmount",
|
||||
"Child (1):willDestroy",
|
||||
"Child (3):mounted",
|
||||
@@ -3114,11 +3109,11 @@ test("t-foreach with dynamic async component", async () => {
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
expect([
|
||||
"Child (2):willDestroy",
|
||||
"Child (3):setup",
|
||||
"Child (3):willStart",
|
||||
"Child (3):willRender",
|
||||
"Child (3):rendered",
|
||||
"Child (2):willDestroy",
|
||||
"Child (1):willUnmount",
|
||||
"Child (1):willDestroy",
|
||||
"Child (3):mounted",
|
||||
@@ -3801,7 +3796,7 @@ test("destroyed component causes other soon to be destroyed component to rerende
|
||||
static template = xml`<t t-esc="state.val + props.value"/>`;
|
||||
state = useState({ val: 0 });
|
||||
setup() {
|
||||
c = this;
|
||||
c = c || this;
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
@@ -3846,8 +3841,6 @@ test("destroyed component causes other soon to be destroyed component to rerende
|
||||
parent.state.valueB = 2;
|
||||
await nextTick();
|
||||
expect([
|
||||
"B:willDestroy",
|
||||
"C:willDestroy",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
@@ -3858,6 +3851,8 @@ test("destroyed component causes other soon to be destroyed component to rerende
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
"B:willDestroy",
|
||||
"C:willDestroy",
|
||||
"A:willPatch",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
@@ -4200,6 +4195,116 @@ test("delayed render is not cancelled by upcoming render", async () => {
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("components are not destroyed between animation frame", async () => {
|
||||
const def = makeDeferred();
|
||||
class C extends Component {
|
||||
static template = xml`C`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
class B extends Component {
|
||||
static template = xml`B<C/>`;
|
||||
static components = { C };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onWillStart(() => {
|
||||
return def;
|
||||
});
|
||||
}
|
||||
}
|
||||
class A extends Component {
|
||||
static template = xml`A<B t-if="state.flag"/>`;
|
||||
static components = { B };
|
||||
|
||||
state = useState({ flag: false });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
const a = await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("A");
|
||||
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
|
||||
|
||||
// turn the flag on, this will render A and stops at B because of def
|
||||
a.state.flag = true;
|
||||
await nextTick();
|
||||
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
|
||||
|
||||
// force a render of A
|
||||
// => owl will need to create a new B component
|
||||
// => initial B component will be cancelled
|
||||
a.render();
|
||||
await nextMicroTick();
|
||||
expect([
|
||||
// note that B is not destroyed here. It is cancelled instead
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
]).toBeLogged();
|
||||
|
||||
// resolve def, so B render is unblocked
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
expect([
|
||||
"B:willRender",
|
||||
"C:setup",
|
||||
"C:willStart",
|
||||
"B:rendered",
|
||||
"C:willRender",
|
||||
"C:rendered",
|
||||
// animation frame callback starts here
|
||||
"B:willDestroy", // B is destroyed here
|
||||
"A:willPatch",
|
||||
"C:mounted",
|
||||
"B:mounted",
|
||||
"A:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("component destroyed just after render", async () => {
|
||||
let stateB: any;
|
||||
|
||||
class B extends Component {
|
||||
static template = xml`B<t t-esc="state.value"/>`;
|
||||
state = useState({ value: 1 });
|
||||
setup() {
|
||||
stateB = this.state;
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
class A extends Component {
|
||||
static template = xml`<B/>`;
|
||||
static components = { B };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
const a = await mount(A, fixture);
|
||||
expect(fixture.innerHTML).toBe("B1");
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"B:rendered",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
stateB!.value++; // force a render of B
|
||||
await nextMicroTick(); // wait for B render to actually start
|
||||
a.__owl__.app.destroy();
|
||||
expect(["A:willUnmount", "B:willUnmount", "B:willDestroy", "A:willDestroy"]).toBeLogged();
|
||||
await nextTick();
|
||||
// check that B was not rendered after being destroyed
|
||||
expect([]).toBeLogged();
|
||||
});
|
||||
|
||||
// test.skip("components with shouldUpdate=false", async () => {
|
||||
// const state = { p: 1, cc: 10 };
|
||||
|
||||
|
||||
@@ -1444,6 +1444,7 @@ describe("can catch errors", () => {
|
||||
"Parent:willPatch",
|
||||
"Child:willUnmount",
|
||||
"Child:willDestroy",
|
||||
"Parent:patched",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
@@ -1506,12 +1507,15 @@ describe("can catch errors", () => {
|
||||
parent.state.hasChild = false;
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Child:willDestroy",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(fixture.innerHTML).toBe("1");
|
||||
await nextTick();
|
||||
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
|
||||
expect(fixture.innerHTML).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -398,4 +398,31 @@ describe("t-call", () => {
|
||||
});
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
test("dynamic t-call with same sub component", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`child`;
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="current.template"/>
|
||||
<t t-call="{{current.template}}"/>`;
|
||||
static components = { Child };
|
||||
current = useState({ template: "A" });
|
||||
}
|
||||
|
||||
const root = await mount(Root, fixture, {
|
||||
templates: `
|
||||
<templates>
|
||||
<t t-name="A"><Child/></t>
|
||||
<t t-name="B"><Child/></t>
|
||||
</templates>`,
|
||||
});
|
||||
expect(fixture.innerHTML).toBe("Achild");
|
||||
|
||||
root.current.template = "B";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Bchild");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -626,6 +626,39 @@ describe("t-model directive", () => {
|
||||
expect(fixture.querySelector("select")!.value).toEqual("b");
|
||||
});
|
||||
|
||||
test("t-model with dynamic number values on select options in foreach", async () => {
|
||||
class Test extends Component {
|
||||
static template = xml`
|
||||
<select t-model.number="state.value">
|
||||
<t t-foreach="state.options" t-as="o" t-key="o.value">
|
||||
<option t-att-value="o.value" t-esc="o.value"/>
|
||||
</t>
|
||||
</select>
|
||||
`;
|
||||
state: any;
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: 2,
|
||||
options: [{ value: 1 }, { value: 2 }, { value: 3 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mount(Test, fixture);
|
||||
// check that we have a value of 2 selected
|
||||
expect(fixture.querySelector("select")!.value).toEqual("2");
|
||||
expect(comp.state.value).toBe(2);
|
||||
|
||||
// emulate a click on the option=3 element
|
||||
fixture.querySelectorAll("option")[2].selected = true;
|
||||
fixture.querySelector("select")!.dispatchEvent(new Event("change"));
|
||||
|
||||
await nextTick();
|
||||
// check that we have now selected the number 3 (and not the string)
|
||||
expect(fixture.querySelector("select")!.value).toEqual("3");
|
||||
expect(comp.state.value).toBe(3);
|
||||
});
|
||||
|
||||
test("t-model is applied before t-on-input", async () => {
|
||||
expect.assertions(3);
|
||||
class SomeComponent extends Component {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.0",
|
||||
"version": "1.1.1",
|
||||
"manifest_version": 3,
|
||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||
"icons": {
|
||||
|
||||
@@ -5,7 +5,7 @@ let owlStatus = 0;
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
// Used to keep track of the tabs where the owl devtools have been opened
|
||||
const activePanels = new Set();
|
||||
const activePanels = new Map();
|
||||
|
||||
// Load the devtools global hook this way when running on manifest v3 chrome
|
||||
if (!IS_FIREFOX) {
|
||||
@@ -75,8 +75,23 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
|
||||
return true;
|
||||
} else if (message.type === "owlStatus") {
|
||||
setOwlStatus(message.data);
|
||||
// Dummy message to test if the extension context is still valid
|
||||
// Refresh panel connection timeout
|
||||
} else if (message.type === "keepAlive") {
|
||||
const panel = activePanels.get(message.id);
|
||||
if (panel) {
|
||||
clearTimeout(panel.expirationTimeout);
|
||||
panel.expirationTimeout = setTimeout(() => {
|
||||
activePanels.delete(message.id);
|
||||
panel.port.disconnect();
|
||||
}, 750);
|
||||
} else {
|
||||
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort_" + message.id });
|
||||
const expirationTimeout = setTimeout(() => {
|
||||
activePanels.delete(message.id);
|
||||
port.disconnect();
|
||||
}, 750);
|
||||
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
|
||||
}
|
||||
return;
|
||||
// Open the devtools documentation in a new tab
|
||||
} else if (message.type === "openDoc") {
|
||||
@@ -87,10 +102,15 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
|
||||
}
|
||||
);
|
||||
return;
|
||||
// Relay the received message to the devtools app
|
||||
// Register a new port for the devtools panel
|
||||
} else if (message.type === "newDevtoolsPanel") {
|
||||
const tab = await getActiveTabURL();
|
||||
activePanels.add(tab);
|
||||
const id = message.id;
|
||||
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort_" + id });
|
||||
const expirationTimeout = setTimeout(() => {
|
||||
activePanels.delete(id);
|
||||
port.disconnect();
|
||||
}, 750);
|
||||
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
|
||||
// This is solely for firefox which doesnt allow access to the chrome.tabs api inside devtools
|
||||
} else if (message.type === "getActiveTabURL") {
|
||||
getActiveTabURL().then((tab) => {
|
||||
@@ -98,15 +118,13 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
const tab = await getActiveTabURL();
|
||||
if (!activePanels.has(tab)) {
|
||||
return;
|
||||
const destinationPanel = activePanels.get(sender.tab.id);
|
||||
if (destinationPanel) {
|
||||
destinationPanel.port.postMessage(
|
||||
message.data
|
||||
? { type: message.type, data: message.data, origin: message.origin }
|
||||
: { type: message.type, origin: message.origin }
|
||||
);
|
||||
}
|
||||
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort" });
|
||||
port.postMessage(
|
||||
message.data
|
||||
? { type: message.type, data: message.data, origin: message.origin }
|
||||
: { type: message.type, origin: message.origin }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ function createPanelsIfOwl() {
|
||||
"../../assets/icon128.png",
|
||||
IS_FIREFOX ? "devtools_panel.html" : "devtools_app/devtools_panel.html"
|
||||
);
|
||||
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel" });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
const { Component, useRef, useEffect } = owl;
|
||||
import { useStore } from "../../../store/store";
|
||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||
import { Subscriptions } from "./subscriptions/subscriptions";
|
||||
|
||||
export class DetailsWindow extends Component {
|
||||
static template = "devtools.DetailsWindow";
|
||||
static components = { ObjectTreeElement, Subscriptions };
|
||||
static components = { ObjectTreeElement };
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
|
||||
+6
-2
@@ -53,7 +53,11 @@
|
||||
</div>
|
||||
<i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i>
|
||||
</div>
|
||||
<Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
|
||||
<div t-if="store.activeComponent.subscriptions.toggled" id="subscriptionsPanel">
|
||||
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
|
||||
<ObjectTreeElement object="subscription.target"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
@@ -72,7 +76,7 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
||||
<ul class="my-1">
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||
<t t-if="store.activeComponent.path.length !== 1">
|
||||
|
||||
+9
-10
@@ -41,23 +41,22 @@ export class ObjectTreeElement extends Component {
|
||||
return JSON.stringify(this.props.object.path);
|
||||
}
|
||||
|
||||
get objectName() {
|
||||
return this.props.object.name;
|
||||
get keyChanges() {
|
||||
return this.props.object.keys?.includes("Symbol(Key changes)");
|
||||
}
|
||||
|
||||
get objectLineClass() {
|
||||
classFor(object) {
|
||||
// Prototype items will be dyed down to appear less important
|
||||
if (this.pathAsString.includes('{"type":"prototype",')) {
|
||||
return { attenuate: true };
|
||||
if (object.path.some((item) => item?.type === "prototype") && !object.keepLit) {
|
||||
return "attenuate";
|
||||
}
|
||||
// Same for subscription items which are not present in the keys while the keys will be bold
|
||||
if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
|
||||
if (this.props.keys.includes(this.props.object.name.toString())) {
|
||||
return { "fw-bolder": true };
|
||||
if (object.objectType === "subscription" && object.depth > 0) {
|
||||
if (this.props.object.keys?.includes(object.name.toString())) {
|
||||
return "fw-bolder";
|
||||
}
|
||||
return { attenuate: true };
|
||||
return "attenuate";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
get objectPadding() {
|
||||
|
||||
+7
-7
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ObjectTreeElement" owl="1">
|
||||
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||
t-att-class="objectLineClass"
|
||||
t-att-class="props.class"
|
||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
>
|
||||
@@ -11,7 +11,7 @@
|
||||
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
|
||||
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||
/>
|
||||
<t t-esc="objectName"/>
|
||||
<t t-esc="props.object.name"/>
|
||||
<t t-if="props.object.content.length > 0">: </t>
|
||||
<t t-if="props.object.contentType == 'getter'">
|
||||
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||
@@ -28,9 +28,10 @@
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
|
||||
<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"'>
|
||||
@@ -38,10 +39,9 @@
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="props.object.toggled">
|
||||
<t t-foreach="props.object.children" t-as="child" t-key="child.name">
|
||||
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
|
||||
<ObjectTreeElement t-else="" object="child"/>
|
||||
<t t-if="props.object.toggled" t-key="contextMenuId">
|
||||
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
|
||||
<ObjectTreeElement object="child" class="this.classFor(child)"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
const { Component } = owl;
|
||||
import { useStore } from "../../../../store/store";
|
||||
import { ObjectTreeElement } from "../object_tree_element/object_tree_element";
|
||||
|
||||
export class Subscriptions extends Component {
|
||||
static template = "devtools.Subscriptions";
|
||||
|
||||
static components = { ObjectTreeElement };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
// Used to display the keys in a compact way
|
||||
keysContent(index) {
|
||||
const keys = this.store.activeComponent.subscriptions.children[index].keys;
|
||||
let content = JSON.stringify(keys);
|
||||
const maxLength = 50;
|
||||
content = content.replace(/,/g, ", ");
|
||||
if (content.length > maxLength) {
|
||||
content = content.slice(0, content.lastIndexOf(",", maxLength - 5)) + ", ...]";
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
expandKeys(event, index) {
|
||||
this.store.activeComponent.subscriptions.children[index].keysExpanded =
|
||||
!this.store.activeComponent.subscriptions.children[index].keysExpanded;
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.Subscriptions" owl="1">
|
||||
<div id="subscriptionsPanel">
|
||||
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
|
||||
<div class="my-2">
|
||||
<div class="my-0 p-0 object-line" t-on-click.stop="(ev) => this.expandKeys(ev, subscription_index)">
|
||||
<span class="ps-1 text-nowrap">
|
||||
<i class="fa fa-caret-right ms-1" t-attf-style="cursor: pointer;{{subscription.keysExpanded ? 'transform: rotate(90deg);' : ''}}"></i>
|
||||
keys: <span class="key-name"><t t-esc="this.keysContent(subscription_index)"/></span>
|
||||
</span>
|
||||
</div>
|
||||
<div t-foreach="subscription.keys" t-as="key" t-key="key_index" class="my-0 p-0 object-line" t-attf-style="display: {{subscription.keysExpanded ? 'flex' : 'none'}}">
|
||||
<div style="transform: translateX(calc(1.1rem))" class="key-content">
|
||||
<i class="fa fa-caret-right mx-1" t-attf-style="cursor: pointer; visibility: hidden;"></i>
|
||||
<t t-esc="key"/>
|
||||
</div>
|
||||
</div>
|
||||
<ObjectTreeElement object="subscription.target" keys="subscription.keys"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
+29
-1
@@ -1,9 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { isElementInCenterViewport, minimizeKey } from "../../../../utils";
|
||||
import { isElementInCenterViewport, minimizeKey, IS_FIREFOX } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
import { HighlightText } from "./highlight_text/highlight_text";
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
const { Component, useRef, useState, useEffect, onMounted } = owl;
|
||||
|
||||
export class TreeElement extends Component {
|
||||
@@ -105,4 +107,30 @@ export class TreeElement extends Component {
|
||||
this.store.selectComponent(this.props.component.path);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the component name to the components toggle blacklist if not already present
|
||||
// Else, remove it from the blacklist
|
||||
toggleComponentToBlacklist() {
|
||||
if (this.store.settings.componentsToggleBlacklist.has(this.props.component.name)) {
|
||||
if (!this.props.component.toggled) {
|
||||
this.props.component.toggled = !this.props.component.toggled;
|
||||
}
|
||||
this.store.settings.componentsToggleBlacklist.delete(this.props.component.name);
|
||||
browserInstance.storage.local.set({
|
||||
owlDevtoolsComponentsToggleBlacklist: Array.from(
|
||||
this.store.settings.componentsToggleBlacklist
|
||||
),
|
||||
});
|
||||
} else {
|
||||
if (this.props.component.toggled) {
|
||||
this.props.component.toggled = !this.props.component.toggled;
|
||||
}
|
||||
this.store.settings.componentsToggleBlacklist.add(this.props.component.name);
|
||||
browserInstance.storage.local.set({
|
||||
owlDevtoolsComponentsToggleBlacklist: Array.from(
|
||||
this.store.settings.componentsToggleBlacklist
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -26,7 +26,7 @@
|
||||
<span t-if="props.component.depth">></span>
|
||||
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
|
||||
<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>
|
||||
@@ -43,6 +43,10 @@
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.Event" owl="1">
|
||||
<div class="event-container">
|
||||
<div class="event-container" t-att-class="{ 'event-last': props.event.isLast }">
|
||||
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
|
||||
<div class="ps-2 text-nowrap">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
@@ -43,7 +43,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="componentContextmenu">
|
||||
<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">
|
||||
|
||||
+2
-2
@@ -28,14 +28,14 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="nodeContextMenu">
|
||||
<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-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="componentContextmenu">
|
||||
<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">
|
||||
|
||||
@@ -11,10 +11,9 @@ export const store = reactive({
|
||||
expandByDefault: true,
|
||||
toggleOnSelected: false,
|
||||
darkmode: false,
|
||||
componentsToggleBlacklist: new Set(),
|
||||
},
|
||||
contextMenu: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
id: 0,
|
||||
activeMenu: -1,
|
||||
// Opens the context menu corresponding with the given menu html element
|
||||
@@ -29,9 +28,9 @@ export const store = reactive({
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y = window.innerHeight - menuHeight;
|
||||
}
|
||||
this.left = x + "px";
|
||||
menu.style.left = x + "px";
|
||||
// Need 25px offset because of the main navbar from the browser devtools
|
||||
this.top = y - 25 + "px";
|
||||
menu.style.top = y - 25 + "px";
|
||||
},
|
||||
// Close the currently displayed context menu
|
||||
close() {
|
||||
@@ -103,21 +102,19 @@ export const store = reactive({
|
||||
if (IS_FIREFOX) {
|
||||
await evalInWindow("window.$0 = $0;", this.activeFrame);
|
||||
}
|
||||
const apps = await evalFunctionInWindow(
|
||||
const [apps, details] = await evalFunctionInWindow(
|
||||
"getComponentsTree",
|
||||
fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
|
||||
fromOld && this.activeComponent
|
||||
? [this.activeComponent.path, this.apps, this.activeComponent]
|
||||
: [],
|
||||
this.activeFrame
|
||||
);
|
||||
this.apps = apps ? apps : [];
|
||||
if (!fromOld && this.settings.expandByDefault) {
|
||||
this.apps.forEach((tree) => expandNodes(tree));
|
||||
this.apps.forEach((tree) => expandNodes(tree, true));
|
||||
}
|
||||
const component = await evalFunctionInWindow(
|
||||
"getComponentDetails",
|
||||
fromOld && this.activeComponent ? [this.activeComponent.path, this.activeComponent] : [],
|
||||
this.activeFrame
|
||||
);
|
||||
this.activeComponent = component;
|
||||
keepEnvLit(details);
|
||||
this.activeComponent = details;
|
||||
},
|
||||
|
||||
// Select a component by retrieving its details from the page based on its path
|
||||
@@ -154,9 +151,11 @@ export const store = reactive({
|
||||
[component.path],
|
||||
this.activeFrame
|
||||
);
|
||||
this.activeComponent = details;
|
||||
if (!this.activeComponent) {
|
||||
if (!details) {
|
||||
await this.loadComponentsTree(false);
|
||||
} else {
|
||||
keepEnvLit(details);
|
||||
this.activeComponent = details;
|
||||
}
|
||||
if (this.page !== "ComponentsTab") {
|
||||
this.switchTab("ComponentsTab");
|
||||
@@ -414,12 +413,7 @@ export const store = reactive({
|
||||
if (!scriptsLoaded) {
|
||||
await loadScripts(frame);
|
||||
}
|
||||
evalInWindow(
|
||||
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
|
||||
store.devtoolsId
|
||||
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
|
||||
frame
|
||||
);
|
||||
evalFunctionInWindow("initDevtools", [frame], frame);
|
||||
if (!this.frameUrls.includes(frame)) {
|
||||
this.frameUrls = [...this.frameUrls, frame];
|
||||
}
|
||||
@@ -503,13 +497,18 @@ export const store = reactive({
|
||||
},
|
||||
|
||||
// Reset all the relevant data about the page currently stored
|
||||
resetData() {
|
||||
async resetData() {
|
||||
await loadSettings();
|
||||
this.loadComponentsTree(false);
|
||||
this.events = [];
|
||||
this.eventsTree = [];
|
||||
this.activeRecorder = false;
|
||||
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
|
||||
this.traceRenderings = false;
|
||||
evalFunctionInWindow("toggleTracing", [false]);
|
||||
this.traceSubscriptions = false;
|
||||
evalFunctionInWindow("toggleSubscriptionTracing", [false]);
|
||||
this.updateIFrameList();
|
||||
},
|
||||
|
||||
// Triggers manually the rendering of the selected component
|
||||
@@ -605,7 +604,7 @@ export const store = reactive({
|
||||
// Refresh the whole extension
|
||||
async refreshExtension() {
|
||||
await loadScripts();
|
||||
this.resetData();
|
||||
await this.resetData();
|
||||
},
|
||||
|
||||
// Toggle dark mode in the extension and store result in the storage
|
||||
@@ -616,7 +615,7 @@ export const store = reactive({
|
||||
} else {
|
||||
document.querySelector("html").classList.remove("dark-mode");
|
||||
}
|
||||
browserInstance.storage.local.set({ owl_devtools_dark_mode: this.settings.darkMode });
|
||||
browserInstance.storage.local.set({ owlDevtoolsDarkMode: this.settings.darkMode });
|
||||
},
|
||||
|
||||
openDocumentation() {
|
||||
@@ -634,7 +633,9 @@ init();
|
||||
async function init() {
|
||||
store.devtoolsId = await getTabURL();
|
||||
|
||||
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||
evalFunctionInWindow("initDevtools", []);
|
||||
|
||||
await loadSettings();
|
||||
|
||||
// We want to load the base components tree when the devtools tab is first opened
|
||||
store.loadComponentsTree(false);
|
||||
@@ -651,114 +652,99 @@ async function init() {
|
||||
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
|
||||
|
||||
// Heartbeat message to test whether the extension context is still valid or not
|
||||
setInterval(() => {
|
||||
if (store.extensionContextStatus) {
|
||||
try {
|
||||
browserInstance.runtime.sendMessage({ type: "keepAlive", id: store.devtoolsId });
|
||||
} catch (e) {
|
||||
store.extensionContextStatus = false;
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Heartbeat message to test whether the extension context is still valid or not
|
||||
setInterval(() => {
|
||||
if (store.extensionContextStatus) {
|
||||
try {
|
||||
browserInstance.runtime.sendMessage({ type: "keepAlive" });
|
||||
} catch (e) {
|
||||
store.extensionContextStatus = false;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
let flushRendersTimeout = false;
|
||||
let rootRendersTimeout = false;
|
||||
// Connect to the port to communicate to the background script
|
||||
browserInstance.runtime.onConnect.addListener((port) => {
|
||||
if (!port.name === "OwlDevtoolsPort") {
|
||||
return;
|
||||
}
|
||||
port.onMessage.addListener(async (msg) => {
|
||||
// Reload the tree after checking if the scripts are loaded when this message is received
|
||||
if (msg.type === "Reload") {
|
||||
const tab = await getTabURL();
|
||||
// Since this message is sent to all devtools windows, only take it into account when this is the active tab
|
||||
if (tab !== store.devtoolsId) {
|
||||
return;
|
||||
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
|
||||
port.onMessage.addListener(async (msg) => {
|
||||
// Reload the tree after checking if the scripts are loaded when this message is received
|
||||
if (msg.type === "Reload") {
|
||||
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
||||
if (store.owlStatus) {
|
||||
evalFunctionInWindow("initDevtools", []);
|
||||
await store.resetData();
|
||||
}
|
||||
}
|
||||
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
||||
if (store.owlStatus) {
|
||||
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||
store.resetData();
|
||||
}
|
||||
}
|
||||
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
|
||||
if (msg.type === "FrameReady") {
|
||||
const tab = await getTabURL();
|
||||
// Same as for the reload message
|
||||
if (tab !== store.devtoolsId) {
|
||||
return;
|
||||
}
|
||||
store.updateIFrameList();
|
||||
store.owlStatus = true;
|
||||
store.resetData();
|
||||
}
|
||||
// We need to reload the components tree when the set of apps in the page is modified
|
||||
if (msg.type === "RefreshApps") {
|
||||
store.loadComponentsTree(true);
|
||||
}
|
||||
// Filter out the messages that are not destined to this devtools tab. The messages above may be sent before
|
||||
// the devtoolsId is set
|
||||
if (msg.origin.id !== store.devtoolsId) {
|
||||
return;
|
||||
}
|
||||
// When message of type Flush is received, overwrite the component tree with the new one from page
|
||||
// A flush message is sent everytime a component is rendered on the page
|
||||
if (msg.type === "Flush") {
|
||||
if (msg.origin.frame !== store.activeFrame) {
|
||||
return;
|
||||
}
|
||||
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
|
||||
return;
|
||||
}
|
||||
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
|
||||
store.renderPaths.add(JSON.stringify(msg.data));
|
||||
clearTimeout(flushRendersTimeout);
|
||||
flushRendersTimeout = setTimeout(() => {
|
||||
store.renderPaths.clear();
|
||||
}, 100);
|
||||
store.loadComponentsTree(true);
|
||||
}
|
||||
// Select the component based on the path received with the SelectElement message
|
||||
if (msg.type === "SelectElement") {
|
||||
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
|
||||
return;
|
||||
}
|
||||
store.selectComponent(msg.data);
|
||||
}
|
||||
// Stop the DOM element selector tool upon receiving the StopSelector message
|
||||
if (msg.type === "StopSelector") {
|
||||
store.componentSearch.activeSelector = false;
|
||||
}
|
||||
|
||||
// Logic for recording an event when the event message is received
|
||||
if (msg.type === "Event") {
|
||||
let events = msg.data;
|
||||
loadEvents(events);
|
||||
}
|
||||
|
||||
// If we know a new iframe has been added to the page, load scripts into it and update the
|
||||
// frames list if it has been directly loaded.
|
||||
if (msg.type === "NewIFrame") {
|
||||
const isLoaded = await loadScripts(msg.data);
|
||||
if (isLoaded) {
|
||||
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
|
||||
if (msg.type === "FrameReady") {
|
||||
store.updateIFrameList();
|
||||
store.owlStatus = true;
|
||||
await store.resetData();
|
||||
}
|
||||
}
|
||||
});
|
||||
// We need to reload the components tree when the set of apps in the page is modified
|
||||
if (msg.type === "RefreshApps") {
|
||||
store.loadComponentsTree(true);
|
||||
}
|
||||
// When message of type Complete is received, overwrite the component tree with the new one from page
|
||||
// A Complete message is sent everytime a root render is triggered on the page
|
||||
if (msg.type === "Complete") {
|
||||
if (msg.origin.frame !== store.activeFrame) {
|
||||
return;
|
||||
}
|
||||
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
|
||||
return;
|
||||
}
|
||||
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
|
||||
store.renderPaths.add(JSON.stringify(msg.data));
|
||||
clearTimeout(rootRendersTimeout);
|
||||
rootRendersTimeout = setTimeout(() => {
|
||||
store.renderPaths.clear();
|
||||
}, 100);
|
||||
store.loadComponentsTree(true);
|
||||
}
|
||||
// Select the component based on the path received with the SelectElement message
|
||||
if (msg.type === "SelectElement") {
|
||||
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
|
||||
return;
|
||||
}
|
||||
store.selectComponent(msg.data);
|
||||
}
|
||||
// Stop the DOM element selector tool upon receiving the StopSelector message
|
||||
if (msg.type === "StopSelector") {
|
||||
store.componentSearch.activeSelector = false;
|
||||
}
|
||||
|
||||
// Logic for recording an event when the event message is received
|
||||
if (msg.type === "Event") {
|
||||
let events = msg.data;
|
||||
loadEvents(events);
|
||||
}
|
||||
|
||||
// If we know a new iframe has been added to the page, load scripts into it and update the
|
||||
// frames list if it has been directly loaded.
|
||||
if (msg.type === "NewIFrame") {
|
||||
const isLoaded = await loadScripts(msg.data);
|
||||
if (isLoaded) {
|
||||
store.updateIFrameList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load all settings from the chrome sync storage
|
||||
async function loadSettings() {
|
||||
let storage = await browserInstance.storage.local.get();
|
||||
if (storage.owl_devtools_dark_mode === undefined) {
|
||||
// Darkmode
|
||||
if (storage.owlDevtoolsDarkMode === undefined) {
|
||||
// Load dark mode based on the global settings of the chrome devtools
|
||||
darkMode = browserInstance.devtools.panels.themeName === "dark";
|
||||
} else {
|
||||
darkMode = storage.owl_devtools_dark_mode;
|
||||
darkMode = storage.owlDevtoolsDarkMode;
|
||||
}
|
||||
store.settings.darkMode = darkMode;
|
||||
if (darkMode) {
|
||||
@@ -766,6 +752,12 @@ async function loadSettings() {
|
||||
} else {
|
||||
document.querySelector("html").classList.remove("dark-mode");
|
||||
}
|
||||
// Components toggle blacklist
|
||||
if (storage.owlDevtoolsComponentsToggleBlacklist !== undefined) {
|
||||
store.settings.componentsToggleBlacklist = new Set(
|
||||
storage.owlDevtoolsComponentsToggleBlacklist
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle and store a batch of events coming from the page
|
||||
@@ -790,6 +782,7 @@ function loadEvents(events) {
|
||||
}
|
||||
event.origin = null;
|
||||
event.toggled = false;
|
||||
event.isLast = false;
|
||||
// Logic to retrace the origin of the event if it is not a root render event
|
||||
if (!event.type.includes("render")) {
|
||||
for (let i = store.events.length - 1; i >= 0; i--) {
|
||||
@@ -839,6 +832,7 @@ function loadEvents(events) {
|
||||
// Make sure we add the event while keeping the whole list ordered by id
|
||||
addEventSorted(event);
|
||||
}
|
||||
store.events[store.events.length - 1].isLast = true;
|
||||
}
|
||||
|
||||
// Deselect component and remove highlight on all children
|
||||
@@ -885,10 +879,39 @@ function highlightChildren(component) {
|
||||
}
|
||||
|
||||
// Expand the node given in entry and all of its children
|
||||
function expandNodes(node) {
|
||||
node.toggled = true;
|
||||
function expandNodes(node, blacklist = false) {
|
||||
if (blacklist && store.settings.componentsToggleBlacklist.has(node.name)) {
|
||||
node.toggled = false;
|
||||
} else {
|
||||
node.toggled = true;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
expandNodes(child);
|
||||
expandNodes(child, blacklist);
|
||||
}
|
||||
}
|
||||
|
||||
// This function transforms the env part of the details such that all env keys are not
|
||||
// greyed out in the UI at their first occurence
|
||||
function keepEnvLit(details) {
|
||||
let alreadyMet = new Set();
|
||||
for (let i = 0; i < details.env.children.length; i++) {
|
||||
if (i < details.env.children.length - 1) {
|
||||
alreadyMet.add(details.env.children[i].name);
|
||||
} else {
|
||||
let lastElement = details.env.children[i];
|
||||
while (lastElement.children.at(-1).name === "[[Prototype]]") {
|
||||
for (const [index, child] of lastElement.children.entries()) {
|
||||
if (index < lastElement.children.length - 1) {
|
||||
if (!alreadyMet.has(child.name)) {
|
||||
child.keepLit = true;
|
||||
alreadyMet.add(child.name);
|
||||
}
|
||||
} else {
|
||||
lastElement = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,10 @@
|
||||
color: var(--prototype-color);
|
||||
}
|
||||
|
||||
.key-changes {
|
||||
background-color: var(--version-bg);
|
||||
}
|
||||
|
||||
.event-container {
|
||||
border-bottom: 1px solid rgb(240, 238, 238);
|
||||
padding-top: 2px!important;
|
||||
@@ -133,6 +137,10 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.event-last {
|
||||
border-bottom: 3px solid rgb(225, 154, 0);
|
||||
}
|
||||
|
||||
.getter-content:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -11,18 +11,17 @@
|
||||
this.Fiber = window.__OWL_DEVTOOLS__.Fiber;
|
||||
// Same but for RootFiber
|
||||
this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber;
|
||||
// Set to keep track of the fibers that are in the flush queue
|
||||
this.queuedFibers = new WeakSet();
|
||||
// This is for retrocompatibility purposes since new versions of owl should always expose toRaw and reactive
|
||||
// in __OWL_DEVTOOLS__
|
||||
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
|
||||
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
|
||||
// Set to keep track of the HTML elements we added to the page
|
||||
this.addedElements = [];
|
||||
// To keep track of the succession order of the render events
|
||||
this.eventId = 0;
|
||||
// Will be reset as soon as a new devtools owl tab is opened. Allows to avoid sending messages to the wrong devtools tab later on
|
||||
this.devtoolsId = 0;
|
||||
// Set to keep track of the frame on which this script is loaded
|
||||
this.frame = "top";
|
||||
// Allows to launch a message each time an iframe html element is added to the page
|
||||
const self = this;
|
||||
const iFrameObserver = new MutationObserver(function (mutationsList) {
|
||||
mutationsList.forEach(function (mutation) {
|
||||
mutation.addedNodes.forEach(function (addedNode) {
|
||||
@@ -38,7 +37,6 @@
|
||||
source: "owl-devtools",
|
||||
type: "NewIFrame",
|
||||
data: addedNode.contentDocument.location.href,
|
||||
origin: { id: self.devtoolsId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -46,11 +44,7 @@
|
||||
});
|
||||
});
|
||||
iFrameObserver.observe(document.body, { subtree: true, childList: true });
|
||||
this.appsPatched = false;
|
||||
this.patchAppsSetMethods();
|
||||
setTimeout(() => {
|
||||
this.patchAppMethods();
|
||||
}, 200);
|
||||
this.recordEvents = false;
|
||||
this.traceRenderings = false;
|
||||
this.traceSubscriptions = false;
|
||||
@@ -123,6 +117,15 @@
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key.toString() + ": " + this.serializeItem(obj[key]);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
return "{" + result.join(", ") + "}";
|
||||
},
|
||||
map(obj) {
|
||||
@@ -170,14 +173,40 @@
|
||||
};
|
||||
}
|
||||
|
||||
initDevtools(frame = "top") {
|
||||
if (!this.devtoolsInit) {
|
||||
this.frame = frame;
|
||||
const self = this;
|
||||
// Flush the events batcher when a root render is completed
|
||||
const original_Complete = self.RootFiber.prototype.complete;
|
||||
self.RootFiber.prototype.complete = function () {
|
||||
original_Complete.call(this, ...arguments);
|
||||
const path = self.getComponentPath(this.node);
|
||||
//Add a functionnality to the complete function which sends a message to the window every time it is triggered.
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "Complete",
|
||||
data: path,
|
||||
origin: { frame: self.frame },
|
||||
});
|
||||
if (self.recordEvents) {
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "Event",
|
||||
data: self.eventsBatch,
|
||||
});
|
||||
self.eventsBatch = [];
|
||||
}
|
||||
};
|
||||
this.devtoolsInit = true;
|
||||
}
|
||||
}
|
||||
// Modify the methods of the apps set in order to send a message each time it is modified.
|
||||
patchAppsSetMethods() {
|
||||
const originalAdd = this.apps.add;
|
||||
const originalDelete = this.apps.delete;
|
||||
const self = this;
|
||||
this.apps.add = function () {
|
||||
originalAdd.call(this, ...arguments);
|
||||
self.patchAppMethods();
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "RefreshApps",
|
||||
@@ -194,39 +223,22 @@
|
||||
|
||||
// Modify methods of each app so that it triggers messages on each flush and component render
|
||||
patchAppMethods() {
|
||||
if (this.appsPatched) {
|
||||
return;
|
||||
}
|
||||
let app;
|
||||
for (const appItem of this.apps) {
|
||||
if (appItem.root) {
|
||||
app = appItem;
|
||||
}
|
||||
}
|
||||
// We don't want to bother patching the apps methods if none have components inside
|
||||
if (!app) {
|
||||
if (!app.root) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
const originalFlush = app.scheduler.constructor.prototype.flush;
|
||||
let inFlush = false;
|
||||
let _render = false;
|
||||
const self = this;
|
||||
app.scheduler.constructor.prototype.flush = function () {
|
||||
// Used to know when a render is triggered inside the flush method or not
|
||||
inFlush = true;
|
||||
[...this.tasks].map((fiber) => {
|
||||
if (fiber.counter === 0 && !self.queuedFibers.has(fiber)) {
|
||||
self.queuedFibers.add(fiber);
|
||||
const path = self.getComponentPath(fiber.node);
|
||||
//Add a functionnality to the flush function which sends a message to the window every time it is triggered.
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "Flush",
|
||||
data: path,
|
||||
origin: { id: self.devtoolsId, frame: self.frame },
|
||||
});
|
||||
}
|
||||
});
|
||||
originalFlush.call(this, ...arguments);
|
||||
inFlush = false;
|
||||
};
|
||||
@@ -323,20 +335,6 @@
|
||||
_render = true;
|
||||
original_Render.call(this, ...arguments);
|
||||
};
|
||||
// Flush the events batcher when a root render is completed
|
||||
const original_Complete = self.RootFiber.prototype.complete;
|
||||
self.RootFiber.prototype.complete = function () {
|
||||
original_Complete.call(this, ...arguments);
|
||||
if (self.recordEvents) {
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "Event",
|
||||
data: self.eventsBatch,
|
||||
origin: { id: self.devtoolsId },
|
||||
});
|
||||
self.eventsBatch = [];
|
||||
}
|
||||
};
|
||||
// Signals when a component is destroyed
|
||||
const originalDestroy = app.root.constructor.prototype._destroy;
|
||||
app.root.constructor.prototype._destroy = function () {
|
||||
@@ -358,7 +356,6 @@
|
||||
originalDestroy.call(this, ...arguments);
|
||||
}
|
||||
};
|
||||
this.appsPatched = true;
|
||||
}
|
||||
|
||||
// patch reactivity system to activate subscription tracing
|
||||
@@ -373,7 +370,7 @@
|
||||
let targetToKeysToCallbacks;
|
||||
|
||||
// Step 1: extract internal values from owl
|
||||
const obj = owl.reactive({}, () => {});
|
||||
const obj = self.reactive({}, () => {});
|
||||
let count = 0;
|
||||
WeakMap.prototype.get = function () {
|
||||
count++;
|
||||
@@ -422,9 +419,14 @@
|
||||
}
|
||||
|
||||
toggleTracing(value) {
|
||||
if (value) {
|
||||
this.patchAppMethods();
|
||||
this.patchAppMethods = () => {}; // to only patch once
|
||||
}
|
||||
this.traceRenderings = value;
|
||||
return this.traceRenderings;
|
||||
}
|
||||
|
||||
toggleSubscriptionTracing(value) {
|
||||
if (value) {
|
||||
this.patchReactivity();
|
||||
@@ -435,6 +437,10 @@
|
||||
}
|
||||
// Enables/disables the recording of the render/destroy events based on value
|
||||
toggleEventsRecording(value, index) {
|
||||
if (value) {
|
||||
this.patchAppMethods();
|
||||
this.patchAppMethods = () => {}; // to only patch once
|
||||
}
|
||||
this.recordEvents = value;
|
||||
this.eventId = index;
|
||||
return this.recordEvents;
|
||||
@@ -609,7 +615,6 @@
|
||||
source: "owl-devtools",
|
||||
type: "SelectElement",
|
||||
data: path,
|
||||
origin: { id: this.devtoolsId },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -637,7 +642,6 @@
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "StopSelector",
|
||||
origin: { id: this.devtoolsId },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -703,7 +707,7 @@
|
||||
}
|
||||
}
|
||||
if (obj) {
|
||||
obj = owl.toRaw(obj);
|
||||
obj = this.toRaw(obj);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
@@ -744,6 +748,9 @@
|
||||
child.contentType = "object";
|
||||
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
|
||||
child.hasChildren = true;
|
||||
if (!oldTree && type === "env") {
|
||||
child.toggled = true;
|
||||
}
|
||||
break;
|
||||
case "set entries":
|
||||
case "map entries":
|
||||
@@ -790,57 +797,48 @@
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (child.contentType) {
|
||||
if (child.toggled) {
|
||||
child.children = this.loadObjectChildren(
|
||||
child.path,
|
||||
child.depth,
|
||||
child.contentType,
|
||||
child.objectType,
|
||||
oldTree
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
if (obj === null) {
|
||||
child.content = "null";
|
||||
child.contentType = "object";
|
||||
child.hasChildren = false;
|
||||
} else if (obj === undefined) {
|
||||
child.content = "undefined";
|
||||
child.contentType = "undefined";
|
||||
child.hasChildren = false;
|
||||
} else {
|
||||
obj = owl.toRaw(obj);
|
||||
switch (true) {
|
||||
case obj instanceof Map:
|
||||
child.contentType = "map";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Set:
|
||||
child.contentType = "set";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Array:
|
||||
child.contentType = "array";
|
||||
child.hasChildren = obj.length > 0;
|
||||
break;
|
||||
case typeof obj === "function":
|
||||
child.contentType = "function";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Object:
|
||||
child.contentType = "object";
|
||||
child.hasChildren = Object.keys(obj).length > 0;
|
||||
break;
|
||||
default:
|
||||
child.contentType = typeof obj;
|
||||
child.hasChildren = false;
|
||||
}
|
||||
if (key.type === "set entry") {
|
||||
child.content = this.serializer.serializeItem(obj, true);
|
||||
if (!child.contentType) {
|
||||
if (obj === null) {
|
||||
child.content = "null";
|
||||
child.contentType = "object";
|
||||
child.hasChildren = false;
|
||||
} else if (obj === undefined) {
|
||||
child.content = "undefined";
|
||||
child.contentType = "undefined";
|
||||
child.hasChildren = false;
|
||||
} else {
|
||||
child.content = this.serializer.serializeContent(obj, child.contentType);
|
||||
obj = this.toRaw(obj);
|
||||
switch (true) {
|
||||
case obj instanceof Map:
|
||||
child.contentType = "map";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Set:
|
||||
child.contentType = "set";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Array:
|
||||
child.contentType = "array";
|
||||
child.hasChildren = obj.length > 0;
|
||||
break;
|
||||
case typeof obj === "function":
|
||||
child.contentType = "function";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Object:
|
||||
child.contentType = "object";
|
||||
child.hasChildren =
|
||||
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
|
||||
break;
|
||||
default:
|
||||
child.contentType = typeof obj;
|
||||
child.hasChildren = false;
|
||||
}
|
||||
if (key.type === "set entry") {
|
||||
child.content = this.serializer.serializeItem(obj, true);
|
||||
} else {
|
||||
child.content = this.serializer.serializeContent(obj, child.contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (child.toggled) {
|
||||
@@ -852,6 +850,7 @@
|
||||
oldTree
|
||||
);
|
||||
}
|
||||
this.addHighlightedKeys(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
@@ -861,7 +860,10 @@
|
||||
let path = completePath.slice(objPathIndex);
|
||||
let obj;
|
||||
if (objType === "subscription") {
|
||||
obj = oldTree.subscriptions.children[path[1].value].target;
|
||||
const subscriptionPath = completePath.slice(0, objPathIndex + 3);
|
||||
obj = oldTree.subscriptions.children.find(
|
||||
(child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
|
||||
).target;
|
||||
path = path.slice(3);
|
||||
} else {
|
||||
// Everything here is in component if it is not an app so remove this key of the path in the former case
|
||||
@@ -891,7 +893,7 @@
|
||||
const children = [];
|
||||
depth = depth + 1;
|
||||
let obj = this.getObjectProperty(path);
|
||||
let oldBranch = this.getObjectInOldTree(oldTree, path, objType);
|
||||
let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType);
|
||||
if (!obj) {
|
||||
return [];
|
||||
}
|
||||
@@ -906,7 +908,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[0],
|
||||
oldBranch?.children[0],
|
||||
oldTree
|
||||
);
|
||||
children.push(mapKey);
|
||||
@@ -916,7 +918,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[1],
|
||||
oldBranch?.children[1],
|
||||
oldTree
|
||||
);
|
||||
children.push(mapValue);
|
||||
@@ -927,7 +929,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[0],
|
||||
oldBranch?.children[0],
|
||||
oldTree
|
||||
);
|
||||
children.push(setValue);
|
||||
@@ -948,7 +950,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
@@ -963,7 +965,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
@@ -980,26 +982,13 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (entries) {
|
||||
children.push(entries);
|
||||
index++;
|
||||
}
|
||||
const size = this.serializeObjectChild(
|
||||
obj,
|
||||
{ type: "item", value: "size", childIndex: children.length },
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (size) {
|
||||
children.push(size);
|
||||
index++;
|
||||
}
|
||||
Reflect.ownKeys(obj).forEach((key) => {
|
||||
const child = this.serializeObjectChild(
|
||||
obj,
|
||||
@@ -1007,7 +996,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
@@ -1042,7 +1031,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) children.push(child);
|
||||
@@ -1075,14 +1064,14 @@
|
||||
});
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
if (!(obj.constructor.name === "Object")) {
|
||||
if (obj.__proto__) {
|
||||
prototype = this.serializeObjectChild(
|
||||
obj,
|
||||
{ type: "prototype", childIndex: children.length },
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children.at(-1),
|
||||
oldBranch?.children.at(-1),
|
||||
oldTree
|
||||
);
|
||||
children.push(prototype);
|
||||
@@ -1287,16 +1276,15 @@
|
||||
children: [],
|
||||
};
|
||||
} else {
|
||||
const rawSubscriptions = node.subscriptions;
|
||||
const rawSubscriptions = this.topLevelSubscriptions(node);
|
||||
component.subscriptions = {
|
||||
toggled: oldTree ? oldTree.subscriptions.toggled : true,
|
||||
children: [],
|
||||
};
|
||||
rawSubscriptions.forEach((rawSubscription, index) => {
|
||||
rawSubscriptions.forEach((rawSubscription) => {
|
||||
let subscription = {
|
||||
keys: [],
|
||||
target: {
|
||||
name: "target",
|
||||
name: this.targetName(rawSubscription.target, node),
|
||||
contentType:
|
||||
typeof rawSubscription.target === "object"
|
||||
? Array.isArray(rawSubscription.target)
|
||||
@@ -1307,28 +1295,20 @@
|
||||
path: [
|
||||
...path,
|
||||
{ type: "item", value: "subscriptions" },
|
||||
{ type: "item", value: index },
|
||||
{ type: "item", value: rawSubscription.index },
|
||||
{ type: "item", value: "target" },
|
||||
],
|
||||
toggled: false,
|
||||
objectType: "subscription",
|
||||
},
|
||||
keysExpanded: false,
|
||||
};
|
||||
if (
|
||||
oldTree &&
|
||||
oldTree.subscriptions.children[index] &&
|
||||
oldTree.subscriptions.children[index].target.toggled
|
||||
oldTree.subscriptions.children[rawSubscription.index] &&
|
||||
oldTree.subscriptions.children[rawSubscription.index].target.toggled
|
||||
) {
|
||||
subscription.target.toggled = true;
|
||||
}
|
||||
rawSubscription.keys.forEach((key) => {
|
||||
if (typeof key === "symbol") {
|
||||
subscription.keys.push(key.toString());
|
||||
} else {
|
||||
subscription.keys.push(key);
|
||||
}
|
||||
});
|
||||
if (rawSubscription.target == null) {
|
||||
if (subscription.target.contentType === "undefined") {
|
||||
subscription.target.content = "undefined";
|
||||
@@ -1358,6 +1338,7 @@
|
||||
oldTree
|
||||
);
|
||||
}
|
||||
this.addHighlightedKeys(subscription.target);
|
||||
component.subscriptions.children.push(subscription);
|
||||
});
|
||||
}
|
||||
@@ -1376,7 +1357,7 @@
|
||||
}
|
||||
getter.hasChildren = false;
|
||||
} else {
|
||||
obj = owl.toRaw(obj);
|
||||
obj = this.toRaw(obj);
|
||||
switch (true) {
|
||||
case obj instanceof Map:
|
||||
getter.contentType = "map";
|
||||
@@ -1465,13 +1446,16 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
const key = path.pop().value;
|
||||
const item = path.pop();
|
||||
const obj = this.getObjectProperty(path);
|
||||
const key = item.hasOwnProperty("symbolIndex")
|
||||
? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
|
||||
: item.value;
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
if (objectType === "subscription") {
|
||||
owl.reactive(obj)[key] = value;
|
||||
this.reactive(obj)[key] = value;
|
||||
} else {
|
||||
obj[key] = value;
|
||||
if (objectType === "props" || objectType === "instance") {
|
||||
@@ -1535,12 +1519,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// If nothing was found, return the first app's root component path
|
||||
return ["0", "root"];
|
||||
// If nothing was found, return the path of the first root component found in the apps
|
||||
const appIndex = [...this.apps].findIndex((app) => app.root);
|
||||
return [appIndex.toString(), "root"];
|
||||
}
|
||||
// Returns the tree of components of the inspected page in a parsed format
|
||||
// Use inspectedPath to specify the path of the selected component
|
||||
getComponentsTree(inspectedPath = null, oldTrees = null) {
|
||||
getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) {
|
||||
const appsArray = [...this.apps];
|
||||
const trees = appsArray.map((app, index) => {
|
||||
let oldTree;
|
||||
@@ -1593,7 +1578,8 @@
|
||||
}
|
||||
return appNode;
|
||||
});
|
||||
return trees ? trees : [];
|
||||
const component = this.getComponentDetails(inspectedPath, oldDetails);
|
||||
return trees ? [trees, component] : [];
|
||||
}
|
||||
// Recursively fills the components tree as a parsed version
|
||||
fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
|
||||
@@ -1689,6 +1675,54 @@
|
||||
inspect(obj);
|
||||
}
|
||||
}
|
||||
|
||||
targetName(target, node) {
|
||||
// check on component
|
||||
const { component } = node;
|
||||
for (const [key, value] of Object.entries(component)) {
|
||||
if (target === this.toRaw(value)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
// check on props
|
||||
for (const [key, value] of Object.entries(component.props)) {
|
||||
if (target === this.toRaw(value)) {
|
||||
return `props.${key}`;
|
||||
}
|
||||
}
|
||||
return "[unknown]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes subscriptions that are a direct child of another subscription:
|
||||
* they will be reachable from the top level by expanding observed keys.
|
||||
*
|
||||
* @param {ComponentNode} node
|
||||
* @returns {{ keys: PropertyKey[], target: unknown}[]} the top level
|
||||
* subscriptions of the node
|
||||
*/
|
||||
topLevelSubscriptions(node) {
|
||||
const subscriptions = node.subscriptions.map((s, index) => ({ ...s, index }));
|
||||
const topLevelValues = new Set(Object.values(node.component).map((o) => this.toRaw(o)));
|
||||
const toOmit = new Set(
|
||||
subscriptions
|
||||
.flatMap(({ keys, target }) => keys.map((k) => this.toRaw(target[k])))
|
||||
.filter((obj) => !topLevelValues.has(obj))
|
||||
);
|
||||
return subscriptions.filter(({ target }) => !toOmit.has(target));
|
||||
}
|
||||
|
||||
addHighlightedKeys(child) {
|
||||
const { path } = child;
|
||||
const subscriptionIndex = path.findIndex((item) => typeof item !== "string");
|
||||
if (path[subscriptionIndex]?.value === "subscriptions") {
|
||||
const node = this.getComponentNode(path.slice(0, subscriptionIndex));
|
||||
// Add observed keys
|
||||
const targetToKeys = new Map(node.subscriptions.map(({ keys, target }) => [target, keys]));
|
||||
const target = this.getObjectProperty(child.path);
|
||||
child.keys = targetToKeys.get(target)?.map((k) => String(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkOwlStatus() {
|
||||
@@ -1748,6 +1782,6 @@
|
||||
false
|
||||
);
|
||||
checkOwlStatus();
|
||||
// Indicates whether the scripts loaded successfully or not (useful when loaded with eval in iframes only)
|
||||
// Indicates whether the scripts loaded successfully or not (only useful when loaded with eval in iframes)
|
||||
return window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user