mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb7d25ba0e | |||
| 5187f01c44 | |||
| 521111644c | |||
| c2728c9daf | |||
| 871dad6a13 | |||
| b620502a0f | |||
| 89cb00cc83 | |||
| 56041bc133 | |||
| e788e361c7 | |||
| 9d378b0e7b | |||
| fd3c194525 |
@@ -320,6 +320,28 @@ class ComponentB extends owl.Component {
|
||||
|
||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||
|
||||
### `slots` prop
|
||||
|
||||
If a component that uses [slots](slots.md) also lists or validates its props, then
|
||||
you will have to explicitely allow the `slots` prop (with an `Object` type), or
|
||||
allow extra props using the `*` notation mentioned above. This is because slots
|
||||
are provided to a component [as props](slots.md#slots-and-props).
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
static props = [someProp, slots?];
|
||||
}
|
||||
|
||||
class MyComponentWithValidation extends Component {
|
||||
static props = {
|
||||
someProp: {type: Number, optional: true},
|
||||
slots : {type: Object, optional: true},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Practices
|
||||
|
||||
A `props` object is a collection of values that come from the parent. As such,
|
||||
|
||||
@@ -193,6 +193,15 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
|
||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||
this will be injected as html.
|
||||
|
||||
`markup` can also be used as a tag function, allowing the interpolated values to
|
||||
be safely escaped:
|
||||
|
||||
```js
|
||||
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||
// <b><script>alert('💥💥')</script></b>
|
||||
const value = markup`<b>${maliciousInput}</b>`;
|
||||
```
|
||||
|
||||
### Setting Variables
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||
|
||||
+146
-37
@@ -276,13 +276,39 @@ function inOwnerDocument(el) {
|
||||
const rootNode = el.getRootNode();
|
||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||
}
|
||||
/**
|
||||
* Determine whether the given element is contained in a specific root documnet:
|
||||
* either directly or with a shadow root in between or in an iframe.
|
||||
*/
|
||||
function isAttachedToDocument(element, documentElement) {
|
||||
let current = element;
|
||||
const shadowRoot = documentElement.defaultView.ShadowRoot;
|
||||
while (current) {
|
||||
if (current === documentElement) {
|
||||
return true;
|
||||
}
|
||||
if (current.parentNode) {
|
||||
current = current.parentNode;
|
||||
}
|
||||
else if (current instanceof shadowRoot && current.host) {
|
||||
current = current.host;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function validateTarget(target) {
|
||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||
const document = target && target.ownerDocument;
|
||||
if (document) {
|
||||
if (!document.defaultView) {
|
||||
throw new OwlError("Cannot mount a component: the target document is not attached to a window (defaultView is missing)");
|
||||
}
|
||||
const HTMLElement = document.defaultView.HTMLElement;
|
||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
||||
if (!isAttachedToDocument(target, document)) {
|
||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
return;
|
||||
@@ -319,12 +345,40 @@ async function loadFile(url) {
|
||||
*/
|
||||
class Markup extends String {
|
||||
}
|
||||
/*
|
||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||
*/
|
||||
function markup(value) {
|
||||
return new Markup(value);
|
||||
function htmlEscape(str) {
|
||||
if (str instanceof Markup) {
|
||||
return str;
|
||||
}
|
||||
if (str === undefined) {
|
||||
return markup("");
|
||||
}
|
||||
if (typeof str === "number") {
|
||||
return markup(String(str));
|
||||
}
|
||||
[
|
||||
["&", "&"],
|
||||
["<", "<"],
|
||||
[">", ">"],
|
||||
["'", "'"],
|
||||
['"', """],
|
||||
["`", "`"],
|
||||
].forEach((pairs) => {
|
||||
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||
});
|
||||
return markup(str);
|
||||
}
|
||||
function markup(valueOrStrings, ...placeholders) {
|
||||
if (!Array.isArray(valueOrStrings)) {
|
||||
return new Markup(valueOrStrings);
|
||||
}
|
||||
const strings = valueOrStrings;
|
||||
let acc = "";
|
||||
let i = 0;
|
||||
for (; i < placeholders.length; ++i) {
|
||||
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||
}
|
||||
acc += strings[i];
|
||||
return new Markup(acc);
|
||||
}
|
||||
|
||||
function createEventHandler(rawEvent) {
|
||||
@@ -3796,7 +3850,16 @@ class CodeTarget {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
const TRANSLATABLE_ATTRS = [
|
||||
"alt",
|
||||
"aria-label",
|
||||
"aria-placeholder",
|
||||
"aria-roledescription",
|
||||
"aria-valuetext",
|
||||
"label",
|
||||
"placeholder",
|
||||
"title",
|
||||
];
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
class CodeGenerator {
|
||||
constructor(ast, options) {
|
||||
@@ -4476,7 +4539,7 @@ class CodeGenerator {
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
let codeIdx = this.target.code.length;
|
||||
if (isNewBlock) {
|
||||
const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;
|
||||
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
|
||||
let result = null;
|
||||
if (n <= 1) {
|
||||
for (let child of ast.content) {
|
||||
@@ -4490,15 +4553,15 @@ class CodeGenerator {
|
||||
let index = 0;
|
||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||
const child = ast.content[i];
|
||||
const isTSet = child.type === 6 /* TSet */;
|
||||
const forceNewBlock = !child.hasNoRepresentation;
|
||||
const subCtx = createContext(ctx, {
|
||||
block,
|
||||
index,
|
||||
forceNewBlock: !isTSet,
|
||||
forceNewBlock,
|
||||
isLast: ctx.isLast && i === l - 1,
|
||||
});
|
||||
this.compileAST(child, subCtx);
|
||||
if (!isTSet) {
|
||||
if (forceNewBlock) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
@@ -4914,11 +4977,11 @@ function parseNode(node, ctx) {
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
@@ -4986,19 +5049,29 @@ function parseTCustom(node, ctx) {
|
||||
function parseTDebugLog(node, ctx) {
|
||||
if (node.hasAttribute("t-debug")) {
|
||||
node.removeAttribute("t-debug");
|
||||
return {
|
||||
const content = parseNode(node, ctx);
|
||||
const ast = {
|
||||
type: 12 /* TDebug */,
|
||||
content: parseNode(node, ctx),
|
||||
content,
|
||||
};
|
||||
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
if (node.hasAttribute("t-log")) {
|
||||
const expr = node.getAttribute("t-log");
|
||||
node.removeAttribute("t-log");
|
||||
return {
|
||||
const content = parseNode(node, ctx);
|
||||
const ast = {
|
||||
type: 13 /* TLog */,
|
||||
expr,
|
||||
content: parseNode(node, ctx),
|
||||
content,
|
||||
};
|
||||
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -5228,11 +5301,19 @@ function parseTKey(node, ctx) {
|
||||
}
|
||||
const key = node.getAttribute("t-key");
|
||||
node.removeAttribute("t-key");
|
||||
const body = parseNode(node, ctx);
|
||||
if (!body) {
|
||||
const content = parseNode(node, ctx);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
return { type: 10 /* TKey */, expr: key, content: body };
|
||||
const ast = {
|
||||
type: 10 /* TKey */,
|
||||
expr: key,
|
||||
content,
|
||||
};
|
||||
if (content.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-call
|
||||
@@ -5341,7 +5422,7 @@ function parseTSetNode(node, ctx) {
|
||||
if (node.textContent !== node.innerHTML) {
|
||||
body = parseChildren(node, ctx);
|
||||
}
|
||||
return { type: 6 /* TSet */, name, value, defaultValue, body };
|
||||
return { type: 6 /* TSet */, name, value, defaultValue, body, hasNoRepresentation: true };
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Components
|
||||
@@ -5520,30 +5601,51 @@ function parseTSlot(node, ctx) {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation
|
||||
// -----------------------------------------------------------------------------
|
||||
function wrapInTTranslationAST(r) {
|
||||
const ast = { type: 16 /* TTranslation */, content: r };
|
||||
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
function parseTTranslation(node, ctx) {
|
||||
if (node.getAttribute("t-translation") !== "off") {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation");
|
||||
return {
|
||||
type: 16 /* TTranslation */,
|
||||
content: parseNode(node, ctx),
|
||||
};
|
||||
const result = parseNode(node, ctx);
|
||||
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
|
||||
const children = result.content.map(wrapInTTranslationAST);
|
||||
return makeASTMulti(children);
|
||||
}
|
||||
return wrapInTTranslationAST(result);
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation Context
|
||||
// -----------------------------------------------------------------------------
|
||||
function wrapInTTranslationContextAST(r, translationCtx) {
|
||||
const ast = {
|
||||
type: 17 /* TTranslationContext */,
|
||||
content: r,
|
||||
translationCtx,
|
||||
};
|
||||
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
function parseTTranslationContext(node, ctx) {
|
||||
const translationCtx = node.getAttribute("t-translation-context");
|
||||
if (!translationCtx) {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation-context");
|
||||
return {
|
||||
type: 17 /* TTranslationContext */,
|
||||
content: parseNode(node, ctx),
|
||||
translationCtx,
|
||||
};
|
||||
const result = parseNode(node, ctx);
|
||||
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
|
||||
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
|
||||
return makeASTMulti(children);
|
||||
}
|
||||
return wrapInTTranslationContextAST(result, translationCtx);
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Portal
|
||||
@@ -5588,6 +5690,13 @@ function parseChildren(node, ctx) {
|
||||
}
|
||||
return children;
|
||||
}
|
||||
function makeASTMulti(children) {
|
||||
const ast = { type: 3 /* Multi */, content: children };
|
||||
if (children.every((c) => c.hasNoRepresentation)) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
/**
|
||||
* Parse all the child nodes of a given node and return an ast if possible.
|
||||
* In the case there are multiple children, they are wrapped in a astmulti.
|
||||
@@ -5600,7 +5709,7 @@ function parseChildNodes(node, ctx) {
|
||||
case 1:
|
||||
return children[0];
|
||||
default:
|
||||
return { type: 3 /* Multi */, content: children };
|
||||
return makeASTMulti(children);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -5704,7 +5813,7 @@ function compile(template, options = {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.6.1";
|
||||
const version = "2.8.1";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -6172,9 +6281,9 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
});
|
||||
};
|
||||
|
||||
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
|
||||
|
||||
__info__.date = '2025-03-05T08:37:58.580Z';
|
||||
__info__.hash = '2b5cea9';
|
||||
__info__.date = '2025-09-23T07:17:45.055Z';
|
||||
__info__.hash = '5211116';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.6.1",
|
||||
"version": "2.8.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.6.1",
|
||||
"version": "2.8.1",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -254,7 +254,16 @@ class CodeTarget {
|
||||
}
|
||||
}
|
||||
|
||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
const TRANSLATABLE_ATTRS = [
|
||||
"alt",
|
||||
"aria-label",
|
||||
"aria-placeholder",
|
||||
"aria-roledescription",
|
||||
"aria-valuetext",
|
||||
"label",
|
||||
"placeholder",
|
||||
"title",
|
||||
];
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
|
||||
export class CodeGenerator {
|
||||
@@ -986,7 +995,7 @@ export class CodeGenerator {
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
let codeIdx = this.target.code.length;
|
||||
if (isNewBlock) {
|
||||
const n = ast.content.filter((c) => c.type !== ASTType.TSet).length;
|
||||
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
|
||||
let result: string | null = null;
|
||||
if (n <= 1) {
|
||||
for (let child of ast.content) {
|
||||
@@ -1000,15 +1009,15 @@ export class CodeGenerator {
|
||||
let index = 0;
|
||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||
const child = ast.content[i];
|
||||
const isTSet = child.type === ASTType.TSet;
|
||||
const forceNewBlock = !child.hasNoRepresentation;
|
||||
const subCtx = createContext(ctx, {
|
||||
block,
|
||||
index,
|
||||
forceNewBlock: !isTSet,
|
||||
forceNewBlock,
|
||||
isLast: ctx.isLast && i === l - 1,
|
||||
});
|
||||
this.compileAST(child, subCtx);
|
||||
if (!isTSet) {
|
||||
if (forceNewBlock) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
+95
-40
@@ -31,12 +31,17 @@ export const enum ASTType {
|
||||
TPortal,
|
||||
}
|
||||
|
||||
export interface ASTText {
|
||||
export interface BaseAST {
|
||||
type: ASTType;
|
||||
hasNoRepresentation?: true;
|
||||
}
|
||||
|
||||
export interface ASTText extends BaseAST {
|
||||
type: ASTType.Text;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ASTComment {
|
||||
export interface ASTComment extends BaseAST {
|
||||
type: ASTType.Comment;
|
||||
value: string;
|
||||
}
|
||||
@@ -52,7 +57,7 @@ interface TModelInfo {
|
||||
specialInitTargetAttr: string | null;
|
||||
}
|
||||
|
||||
export interface ASTDomNode {
|
||||
export interface ASTDomNode extends BaseAST {
|
||||
type: ASTType.DomNode;
|
||||
tag: string;
|
||||
content: AST[];
|
||||
@@ -65,24 +70,24 @@ export interface ASTDomNode {
|
||||
ns: string | null;
|
||||
}
|
||||
|
||||
export interface ASTMulti {
|
||||
export interface ASTMulti extends BaseAST {
|
||||
type: ASTType.Multi;
|
||||
content: AST[];
|
||||
}
|
||||
|
||||
export interface ASTTEsc {
|
||||
export interface ASTTEsc extends BaseAST {
|
||||
type: ASTType.TEsc;
|
||||
expr: string;
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export interface ASTTOut {
|
||||
export interface ASTTOut extends BaseAST {
|
||||
type: ASTType.TOut;
|
||||
expr: string;
|
||||
body: AST[] | null;
|
||||
}
|
||||
|
||||
export interface ASTTif {
|
||||
export interface ASTTif extends BaseAST {
|
||||
type: ASTType.TIf;
|
||||
condition: string;
|
||||
content: AST;
|
||||
@@ -90,15 +95,16 @@ export interface ASTTif {
|
||||
tElse: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTSet {
|
||||
export interface ASTTSet extends BaseAST {
|
||||
type: ASTType.TSet;
|
||||
name: string;
|
||||
value: string | null; // value defined in attribute
|
||||
defaultValue: string | null; // value defined in body, if text
|
||||
body: AST[] | null; // content of body if not text
|
||||
hasNoRepresentation: true;
|
||||
}
|
||||
|
||||
export interface ASTTForEach {
|
||||
export interface ASTTForEach extends BaseAST {
|
||||
type: ASTType.TForEach;
|
||||
collection: string;
|
||||
elem: string;
|
||||
@@ -111,13 +117,13 @@ export interface ASTTForEach {
|
||||
key: string | null;
|
||||
}
|
||||
|
||||
export interface ASTTKey {
|
||||
export interface ASTTKey extends BaseAST {
|
||||
type: ASTType.TKey;
|
||||
expr: string;
|
||||
content: AST;
|
||||
}
|
||||
|
||||
export interface ASTTCall {
|
||||
export interface ASTTCall extends BaseAST {
|
||||
type: ASTType.TCall;
|
||||
name: string;
|
||||
body: AST[] | null;
|
||||
@@ -132,7 +138,7 @@ interface SlotDefinition {
|
||||
attrsTranslationCtx: Attrs | null;
|
||||
}
|
||||
|
||||
export interface ASTComponent {
|
||||
export interface ASTComponent extends BaseAST {
|
||||
type: ASTType.TComponent;
|
||||
name: string;
|
||||
isDynamic: boolean;
|
||||
@@ -143,7 +149,7 @@ export interface ASTComponent {
|
||||
slots: { [name: string]: SlotDefinition } | null;
|
||||
}
|
||||
|
||||
export interface ASTSlot {
|
||||
export interface ASTSlot extends BaseAST {
|
||||
type: ASTType.TSlot;
|
||||
name: string;
|
||||
attrs: Attrs | null;
|
||||
@@ -152,34 +158,34 @@ export interface ASTSlot {
|
||||
defaultContent: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTCallBlock {
|
||||
export interface ASTTCallBlock extends BaseAST {
|
||||
type: ASTType.TCallBlock;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ASTDebug {
|
||||
export interface ASTDebug extends BaseAST {
|
||||
type: ASTType.TDebug;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTLog {
|
||||
export interface ASTLog extends BaseAST {
|
||||
type: ASTType.TLog;
|
||||
expr: string;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTranslation {
|
||||
export interface ASTTranslation extends BaseAST {
|
||||
type: ASTType.TTranslation;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTranslationContext {
|
||||
export interface ASTTranslationContext extends BaseAST {
|
||||
type: ASTType.TTranslationContext;
|
||||
content: AST | null;
|
||||
translationCtx: string;
|
||||
}
|
||||
|
||||
export interface ASTTPortal {
|
||||
export interface ASTTPortal extends BaseAST {
|
||||
type: ASTType.TPortal;
|
||||
target: string;
|
||||
content: AST;
|
||||
@@ -253,11 +259,11 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
@@ -334,20 +340,30 @@ function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
|
||||
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.hasAttribute("t-debug")) {
|
||||
node.removeAttribute("t-debug");
|
||||
return {
|
||||
const content = parseNode(node, ctx);
|
||||
const ast: ASTDebug = {
|
||||
type: ASTType.TDebug,
|
||||
content: parseNode(node, ctx),
|
||||
content,
|
||||
};
|
||||
if (content?.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
if (node.hasAttribute("t-log")) {
|
||||
const expr = node.getAttribute("t-log")!;
|
||||
node.removeAttribute("t-log");
|
||||
return {
|
||||
const content = parseNode(node, ctx);
|
||||
const ast: ASTLog = {
|
||||
type: ASTType.TLog,
|
||||
expr,
|
||||
content: parseNode(node, ctx),
|
||||
content,
|
||||
};
|
||||
if (content?.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -598,11 +614,19 @@ function parseTKey(node: Element, ctx: ParsingContext): AST | null {
|
||||
}
|
||||
const key = node.getAttribute("t-key")!;
|
||||
node.removeAttribute("t-key");
|
||||
const body = parseNode(node, ctx);
|
||||
if (!body) {
|
||||
const content = parseNode(node, ctx);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
return { type: ASTType.TKey, expr: key, content: body };
|
||||
const ast: ASTTKey = {
|
||||
type: ASTType.TKey,
|
||||
expr: key,
|
||||
content,
|
||||
};
|
||||
if (content.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -724,7 +748,7 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.textContent !== node.innerHTML) {
|
||||
body = parseChildren(node, ctx);
|
||||
}
|
||||
return { type: ASTType.TSet, name, value, defaultValue, body };
|
||||
return { type: ASTType.TSet, name, value, defaultValue, body, hasNoRepresentation: true };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -916,32 +940,55 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
||||
// Translation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function wrapInTTranslationAST(r: AST | null) {
|
||||
const ast: ASTTranslation = { type: ASTType.TTranslation, content: r };
|
||||
if (r?.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.getAttribute("t-translation") !== "off") {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation");
|
||||
return {
|
||||
type: ASTType.TTranslation,
|
||||
content: parseNode(node, ctx),
|
||||
};
|
||||
const result = parseNode(node, ctx);
|
||||
if (result?.type === ASTType.Multi) {
|
||||
const children = result.content.map(wrapInTTranslationAST);
|
||||
return makeASTMulti(children);
|
||||
}
|
||||
return wrapInTTranslationAST(result);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation Context
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function wrapInTTranslationContextAST(r: AST | null, translationCtx: string) {
|
||||
const ast: ASTTranslationContext = {
|
||||
type: ASTType.TTranslationContext,
|
||||
content: r,
|
||||
translationCtx,
|
||||
};
|
||||
if (r?.hasNoRepresentation) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
|
||||
const translationCtx = node.getAttribute("t-translation-context");
|
||||
if (!translationCtx) {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation-context");
|
||||
return {
|
||||
type: ASTType.TTranslationContext,
|
||||
content: parseNode(node, ctx),
|
||||
translationCtx,
|
||||
};
|
||||
const result = parseNode(node, ctx);
|
||||
if (result?.type === ASTType.Multi) {
|
||||
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
|
||||
return makeASTMulti(children);
|
||||
}
|
||||
return wrapInTTranslationContextAST(result, translationCtx);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -990,6 +1037,14 @@ function parseChildren(node: Element, ctx: ParsingContext): AST[] {
|
||||
return children;
|
||||
}
|
||||
|
||||
function makeASTMulti(children: AST[]) {
|
||||
const ast: ASTMulti = { type: ASTType.Multi, content: children };
|
||||
if (children.every((c) => c.hasNoRepresentation)) {
|
||||
ast.hasNoRepresentation = true;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all the child nodes of a given node and return an ast if possible.
|
||||
* In the case there are multiple children, they are wrapped in a astmulti.
|
||||
@@ -1002,7 +1057,7 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
||||
case 1:
|
||||
return children[0];
|
||||
default:
|
||||
return { type: ASTType.Multi, content: children };
|
||||
return makeASTMulti(children);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export function handleError(params: ErrorParams) {
|
||||
let current: Fiber | null = fiber;
|
||||
do {
|
||||
current.node.fiber = current;
|
||||
fibersInError.set(current, error);
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
||||
export { status } from "./status";
|
||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
|
||||
export {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
|
||||
+74
-4
@@ -35,13 +35,43 @@ export function inOwnerDocument(el?: HTMLElement) {
|
||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given element is contained in a specific root documnet:
|
||||
* either directly or with a shadow root in between or in an iframe.
|
||||
*/
|
||||
function isAttachedToDocument(
|
||||
element: HTMLElement | ShadowRoot,
|
||||
documentElement: Document
|
||||
): boolean {
|
||||
let current: Node = element;
|
||||
const shadowRoot = documentElement.defaultView!.ShadowRoot;
|
||||
while (current) {
|
||||
if (current === documentElement) {
|
||||
return true;
|
||||
}
|
||||
if (current.parentNode) {
|
||||
current = current.parentNode;
|
||||
} else if (current instanceof shadowRoot && current.host) {
|
||||
current = current.host;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||
const document = target && target.ownerDocument;
|
||||
if (document) {
|
||||
const HTMLElement = document.defaultView!.HTMLElement;
|
||||
if (!document.defaultView) {
|
||||
throw new OwlError(
|
||||
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||
);
|
||||
}
|
||||
const HTMLElement = document.defaultView.HTMLElement;
|
||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
||||
if (!isAttachedToDocument(target, document)) {
|
||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
return;
|
||||
@@ -81,10 +111,50 @@ export async function loadFile(url: string): Promise<string> {
|
||||
*/
|
||||
export class Markup extends String {}
|
||||
|
||||
export function htmlEscape(str: any): Markup {
|
||||
if (str instanceof Markup) {
|
||||
return str;
|
||||
}
|
||||
if (str === undefined) {
|
||||
return markup("");
|
||||
}
|
||||
if (typeof str === "number") {
|
||||
return markup(String(str));
|
||||
}
|
||||
[
|
||||
["&", "&"],
|
||||
["<", "<"],
|
||||
[">", ">"],
|
||||
["'", "'"],
|
||||
['"', """],
|
||||
["`", "`"],
|
||||
].forEach((pairs) => {
|
||||
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||
});
|
||||
return markup(str);
|
||||
}
|
||||
|
||||
/*
|
||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||
*
|
||||
* If called as a tag function, the interpolated strings are escaped.
|
||||
*/
|
||||
export function markup(value: any) {
|
||||
return new Markup(value);
|
||||
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
|
||||
export function markup(value: string): Markup;
|
||||
export function markup(
|
||||
valueOrStrings: string | TemplateStringsArray,
|
||||
...placeholders: unknown[]
|
||||
): Markup {
|
||||
if (!Array.isArray(valueOrStrings)) {
|
||||
return new Markup(valueOrStrings);
|
||||
}
|
||||
const strings = valueOrStrings;
|
||||
let acc = "";
|
||||
let i = 0;
|
||||
for (; i < placeholders.length; ++i) {
|
||||
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||
}
|
||||
acc += strings[i];
|
||||
return new Markup(acc);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.6.1";
|
||||
export const version = "2.8.1";
|
||||
|
||||
@@ -49,6 +49,27 @@ exports[`debugging t-debug on sub template 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`debugging t-debug: interaction with t-set 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
debugger;
|
||||
setContextValue(ctx, \\"foo\\", 42);
|
||||
debugger;
|
||||
setContextValue(ctx, \\"bar\\", 49);
|
||||
let txt1 = ctx['foo']+ctx['bar'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`debugging t-log 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -66,3 +87,24 @@ exports[`debugging t-log 1`] = `
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`debugging t-log: interaction with t-set 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
console.log(ctx['foo']);
|
||||
setContextValue(ctx, \\"foo\\", 42);
|
||||
console.log(ctx['bar']);
|
||||
setContextValue(ctx, \\"bar\\", 49);
|
||||
let txt1 = ctx['foo']+ctx['bar'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -103,3 +103,18 @@ exports[`t-key t-key on sub dom node pushes a child block in its parent 2`] = `
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-key t-key: interaction with t-esc 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p><block-text-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const tKey_1 = ctx['key'];
|
||||
let txt1 = ctx['text'];
|
||||
return toggler(tKey_1, block1([txt1]));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -10,8 +10,7 @@ exports[`translation context body of t-sets are translated in context 1`] = `
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"label\\", \`traduit\`);
|
||||
const b2 = text(ctx['label']);
|
||||
return multi([b2]);
|
||||
return text(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -94,6 +93,23 @@ exports[`translation context slot attrs and text contents are translated in cont
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context t-translation-context with several children 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (true) {
|
||||
b2 = text(\`\`);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context translation of attributes in context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -153,6 +169,21 @@ exports[`translation support body of t-sets inside translation=off are not trans
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support body of t-sets inside translation=off are not translated 2 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
ctx[isBoundary] = 1
|
||||
setContextValue(ctx, \\"label\\", \`untranslated\`);
|
||||
return text(ctx['label']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support body of t-sets with html content are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -264,6 +295,23 @@ exports[`translation support t-set and falsy t-value: t-body are translated 1`]
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support t-translation with several children 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (true) {
|
||||
b2 = text(\`\`);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -692,6 +692,7 @@ describe("qweb parser", () => {
|
||||
value: "value",
|
||||
defaultValue: null,
|
||||
body: null,
|
||||
hasNoRepresentation: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -702,6 +703,7 @@ describe("qweb parser", () => {
|
||||
defaultValue: "ok",
|
||||
value: null,
|
||||
body: null,
|
||||
hasNoRepresentation: true,
|
||||
});
|
||||
|
||||
expect(parse(`<t t-set="v"><div>ok</div></t>`)).toEqual({
|
||||
@@ -723,6 +725,7 @@ describe("qweb parser", () => {
|
||||
content: [{ type: ASTType.Text, value: "ok" }],
|
||||
},
|
||||
],
|
||||
hasNoRepresentation: true,
|
||||
});
|
||||
|
||||
expect(parse(`<t t-set="v"><div>ok</div>abc</t>`)).toEqual({
|
||||
@@ -745,6 +748,7 @@ describe("qweb parser", () => {
|
||||
},
|
||||
{ type: ASTType.Text, value: "abc" },
|
||||
],
|
||||
hasNoRepresentation: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -758,6 +762,7 @@ describe("qweb parser", () => {
|
||||
defaultValue: "ok",
|
||||
value: null,
|
||||
body: null,
|
||||
hasNoRepresentation: true,
|
||||
},
|
||||
tElif: null,
|
||||
tElse: null,
|
||||
@@ -783,7 +788,14 @@ describe("qweb parser", () => {
|
||||
condition: "flag",
|
||||
content: { type: ASTType.Text, value: "1" },
|
||||
tElif: null,
|
||||
tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null },
|
||||
tElse: {
|
||||
type: ASTType.TSet,
|
||||
name: "ourvar",
|
||||
value: "0",
|
||||
defaultValue: null,
|
||||
body: null,
|
||||
hasNoRepresentation: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1971,6 +1983,54 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('t-translation="off": interaction with t-esc', async () => {
|
||||
expect(parse(`<span t-esc="a" t-translation="off"/>`)).toEqual({
|
||||
type: ASTType.TTranslation,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
defaultValue: "",
|
||||
expr: "a",
|
||||
type: ASTType.TEsc,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('t-translation="off": interaction with t-out', async () => {
|
||||
expect(parse(`<span t-out="a" t-translation="off"/>`)).toEqual({
|
||||
type: ASTType.TTranslation,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
body: null,
|
||||
expr: "a",
|
||||
type: ASTType.TOut,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-translation-context
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2008,6 +2068,56 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("t-translation-context: interaction with t-esc", async () => {
|
||||
expect(parse(`<span t-esc="a" t-translation-context="fr"/>`)).toEqual({
|
||||
type: ASTType.TTranslationContext,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
defaultValue: "",
|
||||
expr: "a",
|
||||
type: ASTType.TEsc,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
translationCtx: "fr",
|
||||
});
|
||||
});
|
||||
|
||||
test("t-translation-context: interaction with t-out", async () => {
|
||||
expect(parse(`<span t-out="a" t-translation-context="fr"/>`)).toEqual({
|
||||
type: ASTType.TTranslationContext,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
body: null,
|
||||
expr: "a",
|
||||
type: ASTType.TOut,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
translationCtx: "fr",
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-translation-context-attr
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -38,4 +38,34 @@ describe("debugging", () => {
|
||||
expect(console.log).toHaveBeenCalledWith(45);
|
||||
console.log = consoleLog;
|
||||
});
|
||||
|
||||
test("t-log: interaction with t-set", () => {
|
||||
const consoleLog = console.log;
|
||||
console.log = jest.fn();
|
||||
|
||||
const template = `
|
||||
<t>
|
||||
<t t-log="foo" t-set="foo" t-value="42"/>
|
||||
<t t-log="bar" t-set="bar" t-value="49"/>
|
||||
<span t-esc="foo + bar"/>
|
||||
</t>
|
||||
`;
|
||||
snapshotTemplate(template);
|
||||
renderToString(template);
|
||||
expect(console.log).toHaveBeenCalledWith(undefined);
|
||||
expect(console.log).toHaveBeenCalledWith(undefined);
|
||||
console.log = consoleLog;
|
||||
});
|
||||
|
||||
test("t-debug: interaction with t-set", () => {
|
||||
const template = `
|
||||
<t>
|
||||
<t t-debug="" t-set="foo" t-value="42"/>
|
||||
<t t-debug="" t-set="bar" t-value="49"/>
|
||||
<span t-esc="foo + bar"/>
|
||||
</t>
|
||||
`;
|
||||
snapshotTemplate(template);
|
||||
renderToString(template);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,4 +63,10 @@ describe("t-key", () => {
|
||||
|
||||
expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>");
|
||||
});
|
||||
|
||||
test("t-key: interaction with t-esc", async () => {
|
||||
const template = `<p t-key="key" t-esc="text"/>`;
|
||||
|
||||
expect(renderToString(template, { key: "1", text: "abc" })).toBe("<p>abc</p>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +129,21 @@ describe("translation support", () => {
|
||||
expect(fixture.innerHTML).toBe("untranslated");
|
||||
});
|
||||
|
||||
test("body of t-sets inside translation=off are not translated 2", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t>
|
||||
<t t-translation="off" t-set="label">untranslated</t>
|
||||
<t t-esc="label"/>
|
||||
</t>`;
|
||||
}
|
||||
|
||||
const translateFn = () => "translated";
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("untranslated");
|
||||
});
|
||||
|
||||
test("body of t-sets with html content are translated", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
@@ -170,6 +185,22 @@ describe("translation support", () => {
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("translated");
|
||||
});
|
||||
|
||||
test("t-translation with several children", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-translation="off">
|
||||
<div/>
|
||||
<div/>
|
||||
</t>
|
||||
<t t-if="true"/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mount(SomeComponent, fixture);
|
||||
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("translation context", () => {
|
||||
@@ -293,4 +324,20 @@ describe("translation context", () => {
|
||||
expect(translateFn).toHaveBeenCalledWith("param", "fr");
|
||||
expect(translateFn).toHaveBeenCalledWith("title", "pt");
|
||||
});
|
||||
|
||||
test("t-translation-context with several children", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-translation-context="ctx">
|
||||
<div/>
|
||||
<div/>
|
||||
</t>
|
||||
<t t-if="true"/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mount(SomeComponent, fixture);
|
||||
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,6 +94,52 @@ exports[`basics no component catching error lead to full app destruction 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics render from above on error -- handler is not a Root or MountFiber 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Parent\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics render from above on error -- handler is not a Root or MountFiber 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Boom\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2, b3;
|
||||
if (ctx['error']) {
|
||||
b2 = text(\`Error\`);
|
||||
} else {
|
||||
b3 = comp1({onError: (ctx['handleError']).bind(this)}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics render from above on error -- handler is not a Root or MountFiber 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['a'].b.c;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics simple catchError 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -233,6 +233,44 @@ function(app, bdom, helpers) {
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("render from above on error -- handler is not a Root or MountFiber", async () => {
|
||||
class Boom extends Component {
|
||||
static template = xml`<div t-esc="a.b.c"/>`;
|
||||
setup() {
|
||||
onError((err) => {
|
||||
this.props.onError(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="error">Error</t>
|
||||
<t t-else="">
|
||||
<Boom onError.bind="handleError"/>
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { Boom };
|
||||
|
||||
error: any = false;
|
||||
|
||||
handleError(err: Error) {
|
||||
this.error = err;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
class GrandParent extends Component {
|
||||
static template: string = xml`<Parent />`;
|
||||
static components = { Parent };
|
||||
}
|
||||
await mount(GrandParent, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>Error</div>");
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors and promises", () => {
|
||||
|
||||
@@ -27,6 +27,58 @@ exports[`shadow_dom can mount app 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom can mount app in closed shadow dom 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom can mount app inside a separate HTML document 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom can mount app inside a shadow child element 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom useRef hook 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -29,6 +29,24 @@ describe("shadow_dom", () => {
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("can mount app in closed shadow dom", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div class="my-div"/>`;
|
||||
}
|
||||
|
||||
const container = document.createElement("div");
|
||||
fixture.appendChild(container);
|
||||
const shadow = container.attachShadow({ mode: "closed" });
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(shadow);
|
||||
const div = shadow.querySelector(".my-div");
|
||||
expect(div).not.toBe(null);
|
||||
expect(shadow.contains(div)).toBe(true);
|
||||
app.destroy();
|
||||
expect(shadow.contains(div)).toBe(false);
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("can bind event handler", async () => {
|
||||
let a = 1;
|
||||
class SomeComponent extends Component {
|
||||
@@ -64,4 +82,73 @@ describe("shadow_dom", () => {
|
||||
await mountedProm;
|
||||
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
||||
});
|
||||
|
||||
test("can mount app inside a shadow child element", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div class="my-div"/>`;
|
||||
}
|
||||
const shadow = fixture.attachShadow({ mode: "open" });
|
||||
const shadowDiv = document.createElement("div");
|
||||
shadow.append(shadowDiv);
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(shadowDiv);
|
||||
const div = shadow.querySelector(".my-div");
|
||||
expect(div).not.toBe(null);
|
||||
expect(shadow.contains(div)).toBe(true);
|
||||
app.destroy();
|
||||
expect(shadow.contains(div)).toBe(false);
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("can mount app inside a separate HTML document", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div class="my-div"/>`;
|
||||
}
|
||||
|
||||
const separateDoc = document.implementation.createHTMLDocument();
|
||||
const container = separateDoc.createElement("div");
|
||||
separateDoc.body.appendChild(container);
|
||||
|
||||
const app = new App(SomeComponent);
|
||||
let error: Error;
|
||||
try {
|
||||
await app.mount(container);
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||
);
|
||||
});
|
||||
|
||||
test("can mount app inside an element in a shadow root inside an iframe", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div class="my-div"/>`;
|
||||
}
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
fixture.appendChild(iframe);
|
||||
|
||||
const iframeDoc = iframe.contentDocument!;
|
||||
const container = iframeDoc.createElement("div");
|
||||
iframeDoc.body.appendChild(container);
|
||||
|
||||
const shadow = container.attachShadow({ mode: "open" });
|
||||
|
||||
const shadowTarget = iframeDoc.createElement("div");
|
||||
shadow.appendChild(shadowTarget);
|
||||
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(shadowTarget);
|
||||
|
||||
const div = shadow.querySelector(".my-div");
|
||||
expect(div).not.toBe(null);
|
||||
expect(shadow.contains(div)).toBe(true);
|
||||
expect(iframeDoc.body.contains(container)).toBe(true);
|
||||
|
||||
app.destroy();
|
||||
expect(shadow.contains(div)).toBe(false);
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
});
|
||||
});
|
||||
|
||||
+92
-1
@@ -1,4 +1,4 @@
|
||||
import { batched, EventBus } from "../src/runtime/utils";
|
||||
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||
import { nextMicroTick } from "./helpers";
|
||||
|
||||
describe("event bus behaviour", () => {
|
||||
@@ -71,3 +71,94 @@ describe("batched", () => {
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
const Markup = markup("").constructor;
|
||||
describe("markup", () => {
|
||||
test("string is flagged as safe", () => {
|
||||
const html = markup("<blink>Hello</blink>");
|
||||
expect(html).toBeInstanceOf(Markup);
|
||||
});
|
||||
describe("htmlEscape", () => {
|
||||
test("htmlEscape escapes text", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape keeps html markup", () => {
|
||||
const res = htmlEscape(markup("<p>test</p>"));
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape produces empty string on undefined", () => {
|
||||
const res = htmlEscape(undefined);
|
||||
expect(res.toString()).toBe("");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape produces string from number", () => {
|
||||
const res = htmlEscape(10);
|
||||
expect(res.toString()).toBe("10");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape produces string from boolean", () => {
|
||||
const res = htmlEscape(false);
|
||||
expect(res.toString()).toBe("false");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape correctly escapes various links", () => {
|
||||
expect(htmlEscape("<a>this is a link</a>").toString()).toBe(
|
||||
"<a>this is a link</a>"
|
||||
);
|
||||
expect(htmlEscape(`<a href="https://www.odoo.com">odoo<a>`).toString()).toBe(
|
||||
`<a href="https://www.odoo.com">odoo<a>`
|
||||
);
|
||||
expect(htmlEscape(`<a href='https://www.odoo.com'>odoo<a>`).toString()).toBe(
|
||||
`<a href='https://www.odoo.com'>odoo<a>`
|
||||
);
|
||||
expect(htmlEscape("<a href='https://www.odoo.com'>Odoo`s website<a>").toString()).toBe(
|
||||
`<a href='https://www.odoo.com'>Odoo`s website<a>`
|
||||
);
|
||||
});
|
||||
test("htmlEscape doesn't escape already escaped content", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
const res2 = htmlEscape(res);
|
||||
expect(res2.toString()).toBe("<p>test</p>");
|
||||
expect(res2).toBeInstanceOf(Markup);
|
||||
expect(res2).toBe(res);
|
||||
});
|
||||
test("htmlEscape returns markup even for only-safe text", () => {
|
||||
const res = htmlEscape("safe");
|
||||
expect(res.toString()).toBe("safe");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
});
|
||||
describe("tag function", () => {
|
||||
test("interpolated values are escaped", () => {
|
||||
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||
const html = markup`<b>${maliciousInput}</b>`;
|
||||
expect(html.toString()).toBe("<b><script>alert('💥💥')</script></b>");
|
||||
expect(html).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("interpolated markups aren't escaped", () => {
|
||||
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
|
||||
const shouldnt = markup("<b>this is safe</b>");
|
||||
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
|
||||
expect(html.toString()).toBe(
|
||||
"<div><script>alert('should be escaped')</script> <b>this is safe</b></div>"
|
||||
);
|
||||
expect(html).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("quotes in interpolated values are escaped", () => {
|
||||
const imgUrl = `lol" onerror="alert('xss')`;
|
||||
const html = markup`<img src="${imgUrl}">`;
|
||||
expect(html.toString()).toBe(`<img src="lol" onerror="alert('xss')">`);
|
||||
});
|
||||
test("already escaped content is not escaped again", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
const html = markup`${res}`;
|
||||
expect(html.toString()).toBe("<p>test</p>");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user