From 46fdfdbd84c790719c5c96f2eeb5b62d200a1e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 8 Jul 2021 10:01:38 +0000 Subject: [PATCH] [IMP] web: update owl from 1.3.2 to 1.4.3 Github releases: https://github.com/odoo/owl/releases/tag/v1.4.0 https://github.com/odoo/owl/releases/tag/v1.4.1 https://github.com/odoo/owl/releases/tag/v1.4.2 https://github.com/odoo/owl/releases/tag/v1.4.3 closes odoo/odoo#73436 Signed-off-by: Aaron Bohy (aab) --- addons/web/static/lib/owl/owl.js | 192 ++++++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 30 deletions(-) diff --git a/addons/web/static/lib/owl/owl.js b/addons/web/static/lib/owl/owl.js index fc748477910..b195cfad04c 100644 --- a/addons/web/static/lib/owl/owl.js +++ b/addons/web/static/lib/owl/owl.js @@ -205,7 +205,7 @@ let tokenizeString = function (expr) { let s = expr[0]; let start = s; - if (s !== "'" && s !== '"') { + if (s !== "'" && s !== '"' && s !== "`") { return false; } let i = 1; @@ -227,6 +227,17 @@ throw new Error("Invalid expression"); } s += start; + if (start === "`") { + return { + type: "TEMPLATE_STRING", + value: s, + replace(replacer) { + return s.replace(/\$\{(.*?)\}/g, (match, group) => { + return "${" + replacer(group) + "}"; + }); + }, + }; + } return { type: "VALUE", value: s }; }; let tokenizeNumber = function (expr) { @@ -320,6 +331,8 @@ //------------------------------------------------------------------------------ // Expression "evaluator" //------------------------------------------------------------------------------ + const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA"); + const isRightSeparator = (token) => token && (token.type === "RIGHT_BRACE" || token.type === "COMMA"); /** * This is the main function exported by this file. This is the code that will * process an expression (given as a string) and returns another expression with @@ -348,13 +361,32 @@ function compileExprToArray(expr, scope) { scope = Object.create(scope); const tokens = tokenize(expr); - for (let i = 0; i < tokens.length; i++) { + let i = 0; + let stack = []; // to track last opening [ or { + while (i < tokens.length) { let token = tokens[i]; let prevToken = tokens[i - 1]; let nextToken = tokens[i + 1]; + let groupType = stack[stack.length - 1]; + switch (token.type) { + case "LEFT_BRACE": + case "LEFT_BRACKET": + stack.push(token.type); + break; + case "RIGHT_BRACE": + case "RIGHT_BRACKET": + stack.pop(); + } let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value); if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { if (prevToken) { + // normalize missing tokens: {a} should be equivalent to {a:a} + if (groupType === "LEFT_BRACE" && + isLeftSeparator(prevToken) && + isRightSeparator(nextToken)) { + tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token }); + nextToken = tokens[i + 1]; + } if (prevToken.type === "OPERATOR" && prevToken.value === ".") { isVar = false; } @@ -365,6 +397,9 @@ } } } + if (token.type === "TEMPLATE_STRING") { + token.value = token.replace((expr) => compileExpr(expr, scope)); + } if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") { if (token.type === "RIGHT_PAREN") { let j = i - 1; @@ -390,6 +425,7 @@ token.value = `scope['${token.value}']`; } } + i++; } return tokens; } @@ -793,7 +829,8 @@ klass = klass || {}; elm = vnode.elm; for (name in oldClass) { - if (name && !klass[name]) { + if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { + // was `true` and now not provided elm.classList.remove(name); } } @@ -1401,20 +1438,53 @@ } const UTILS = { zero: Symbol("zero"), - toObj(expr) { + toClassObj(expr) { + const result = {}; if (typeof expr === "string") { + // we transform here a list of classes into an object: + // 'hey you' becomes {hey: true, you: true} expr = expr.trim(); if (!expr) { return {}; } let words = expr.split(/\s+/); - let result = {}; for (let i = 0; i < words.length; i++) { result[words[i]] = true; } return result; } - return expr; + // this is already an object, but we may need to split keys: + // {'a': true, 'b c': true} should become {a: true, b: true, c: true} + for (let key in expr) { + const value = expr[key]; + const words = key.split(/\s+/); + for (let word of words) { + result[word] = value; + } + } + return result; + }, + /** + * This method combines the current context with the variables defined in a + * scope for use in a slot. + * + * The implementation is kind of tricky because we want to preserve the + * prototype chain structure of the cloned result. So we need to traverse the + * prototype chain, cloning each level respectively. + */ + combine(context, scope) { + let clone = context; + const scopeStack = []; + while (!isComponent(scope)) { + scopeStack.push(scope); + scope = scope.__proto__; + } + while (scopeStack.length) { + let scope = scopeStack.pop(); + clone = Object.create(clone); + Object.assign(clone, scope); + } + return clone; }, shallowEqual, addNameSpace(vnode) { @@ -1832,13 +1902,21 @@ } } if (node.nodeName !== "t" || node.hasAttribute("t-tag")) { - let nodeID = this._compileGenericNode(node, ctx, withHandlers); - ctx = ctx.withParent(nodeID); let nodeHooks = {}; let addNodeHook = function (hook, handler) { nodeHooks[hook] = nodeHooks[hook] || []; nodeHooks[hook].push(handler); }; + if (node.tagName === "select" && node.hasAttribute("t-att-value")) { + const value = node.getAttribute("t-att-value"); + let exprId = ctx.generateID(); + ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`); + let expr = `expr${exprId}`; + node.setAttribute("t-att-value", expr); + addNodeHook("create", `n.elm.value=${expr};`); + } + let nodeID = this._compileGenericNode(node, ctx, withHandlers); + ctx = ctx.withParent(nodeID); for (let { directive, value, fullName } of validDirectives) { if (directive.atNodeCreation) { directive.atNodeCreation({ @@ -1909,16 +1987,18 @@ isProp = key === "selected" || key === "disabled"; break; case "textarea": - isProp = key === "readonly" || key === "disabled"; + isProp = key === "readonly" || key === "disabled" || key === "value"; + break; + case "select": + isProp = key === "disabled" || key === "value"; break; case "button": - case "select": case "optgroup": isProp = key === "disabled"; break; } if (isProp) { - props.push(`${key}: _${val}`); + props.push(`${key}: ${val}`); } } let classObj = ""; @@ -1953,7 +2033,7 @@ name = '"' + name + '"'; } attrs.push(`${name}: _${attID}`); - handleProperties(name, attID); + handleProperties(name, `_${attID}`); } } // dynamic attributes @@ -1963,7 +2043,7 @@ let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`; if (attName === "class") { ctx.rootContext.shouldDefineUtils = true; - formattedValue = `utils.toObj(${formattedValue})`; + formattedValue = `utils.toClassObj(${formattedValue})`; if (classObj) { ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); } @@ -1988,9 +2068,15 @@ const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":")); attrs.splice(attrIndex, 1); } - ctx.addLine(`let _${attID} = ${formattedValue};`); - attrs.push(`${attName}: _${attID}`); - handleProperties(attName, attID); + if (node.nodeName === "select" && attName === "value") { + attrs.push(`${attName}: ${v}`); + handleProperties(attName, v); + } + else { + ctx.addLine(`let _${attID} = ${formattedValue};`); + attrs.push(`${attName}: _${attID}`); + handleProperties(attName, "_" + attID); + } } } if (name.startsWith("t-attf-")) { @@ -2540,6 +2626,7 @@ // we need to capture every variable in it putInCache = false; code = ctx.captureExpression(value); + code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`; } const modCode = mods.map((mod) => modcodes[mod]).join(""); let handler = `function (e) {if (context.__owl__.status === ${5 /* DESTROYED */}){return}${modCode}${code}}`; @@ -2647,7 +2734,15 @@ const durations = (styles.transitionDuration || "").split(", "); const timeout = getTimeout(delays, durations); if (timeout > 0) { - elm.addEventListener("transitionend", cb, { once: true }); + const transitionEndCB = () => { + if (!elm.parentNode) + return; + cb(); + browser.clearTimeout(fallbackTimeout); + elm.removeEventListener("transitionend", transitionEndCB); + }; + elm.addEventListener("transitionend", transitionEndCB, { once: true }); + const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1); } else { cb(); @@ -3087,7 +3182,19 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor .map((k) => k + ":" + props[k]) .join(","); let componentID = ctx.generateID(); - const templateKey = ctx.generateTemplateKey(); + let hasDefinedKey = false; + let templateKey; + if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) { + defineComponentKey(); + const id = ctx.generateID(); + // the ___ is to make sure we have no possible conflict with normal + // template keys + ctx.addLine(`let k${id} = '___' + componentKey${componentID}`); + templateKey = `k${id}`; + } + else { + templateKey = ctx.generateTemplateKey(); + } let ref = node.getAttribute("t-ref"); let refExpr = ""; let refKey = ""; @@ -3132,7 +3239,7 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor if (tattClass) { let tattExpr = ctx.formatExpression(tattClass); if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") { - tattExpr = `utils.toObj(${tattExpr})`; + tattExpr = `utils.toClassObj(${tattExpr})`; } if (classAttr) { ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`); @@ -3169,7 +3276,7 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor } if (hasDynamicProps) { const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")); - ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`); + ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`); } else { ctx.addLine(`let props${componentID} = {${propStr}};`); @@ -3184,7 +3291,7 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor } // SLOTS const hasSlots = node.childNodes.length; - let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined"; + let scope = hasSlots ? `utils.combine(context, scope)` : "undefined"; ctx.addIf(`w${componentID}`); // need to update component let styleCode = ""; @@ -3201,9 +3308,15 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor } ctx.addElse(); // new component + function defineComponentKey() { + if (!hasDefinedKey) { + const interpValue = ctx.interpolate(value); + ctx.addLine(`let componentKey${componentID} = ${interpValue};`); + hasDefinedKey = true; + } + } + defineComponentKey(); const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value); - const interpValue = ctx.interpolate(value); - ctx.addLine(`let componentKey${componentID} = ${interpValue};`); ctx.addLine(`let W${componentID} = ${contextualValue} || context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`); // maybe only do this in dev mode... ctx.addLine(`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`); @@ -3263,12 +3376,17 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor } } if (clone.childNodes.length) { + let hasContent = false; const t = clone.ownerDocument.createElement("t"); for (let child of Object.values(clone.childNodes)) { + hasContent = + hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true); t.appendChild(child); } - const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true }); - QWeb.slots[`${slotId}_default`] = slotFn; + if (hasContent) { + const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true }); + QWeb.slots[`${slotId}_default`] = slotFn; + } } } ctx.addLine(`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`); @@ -3689,7 +3807,15 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor this.root.counter = 0; this.root.error = error; scheduler.flush(); - root.destroy(); + // at this point, the state of the application is corrupted and we could + // have a lot of issues or crashes. So we destroy the application in a try + // catch and swallow these errors because the fiber is already in error, + // and this is the actual issue that needs to be solved, not those followup + // errors. + try { + root.destroy(); + } + catch (e) { } } } } @@ -5227,7 +5353,10 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor const initialParams = this.currentParams; const result = await this.matchAndApplyRules(path); if (result.type === "match") { - const finalPath = this.routeToPath(result.route, result.params); + let finalPath = this.routeToPath(result.route, result.params); + if (path.indexOf("?") > -1) { + finalPath += "?" + path.split("?")[1]; + } const isPopStateEvent = ev && ev instanceof PopStateEvent; if (!isPopStateEvent) { this.setUrlFromPath(finalPath); @@ -5328,6 +5457,9 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor if (route.path === "*") { return {}; } + if (path.indexOf("?") > -1) { + path = path.split("?")[0]; + } if (path.startsWith("#")) { path = path.slice(1); } @@ -5412,9 +5544,9 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor exports.utils = utils; - __info__.version = '1.3.2'; - __info__.date = '2021-06-18T08:42:12.863Z'; - __info__.hash = '2e83c73'; + __info__.version = '1.4.3'; + __info__.date = '2021-07-08T14:07:50.809Z'; + __info__.hash = '9fe2da7'; __info__.url = 'https://github.com/odoo/owl';