diff --git a/tools/benchmarks/owl-0.10.0/app.js b/tools/benchmarks/owl-0.10.0/app.js deleted file mode 100644 index f89a020a..00000000 --- a/tools/benchmarks/owl-0.10.0/app.js +++ /dev/null @@ -1,173 +0,0 @@ -import { - buildData, - startMeasure, - stopMeasure, - formatNumber -} from "../shared/utils.js"; - -//------------------------------------------------------------------------------ -// Likes Counter Widget -//------------------------------------------------------------------------------ -class Counter extends owl.Component { - state = { counter: 0 }; - template = "Counter"; - - increment() { - this.state.counter++; - } -} - -//------------------------------------------------------------------------------ -// Message Widget -//------------------------------------------------------------------------------ -class Message extends owl.Component { - widgets = { Counter }; - template = "Message"; - - shouldUpdate(nextProps) { - return nextProps !== this.props; - } - removeMessage() { - this.trigger("remove_message", { - id: this.props.id - }); - } -} - -//------------------------------------------------------------------------------ -// Root Widget -//------------------------------------------------------------------------------ -class App extends owl.Component { - widgets = { Message }; - state = { messages: [], multipleFlag: false, clearAfterFlag: false }; - template = "App"; - - mounted() { - this.log( - `Benchmarking Owl v${owl._version} (build date: ${ - owl._date - })` - ); - } - - benchmark(message, fn, callback) { - if (this.state.multipleFlag) { - const N = 20; - let n = N; - let total = 0; - let cb = info => { - let finalize = () => { - n--; - total += info.delta; - if (n === 0) { - const avg = total / N; - this.log(`Average: ${formatNumber(avg)}ms`, true); - if (callback) { - callback(); - } - } else { - this._benchmark(message, fn, cb); - } - }; - - if (this.state.clearAfterFlag) { - this._benchmark( - "clear", - () => { - this.state.messages = []; - }, - finalize, - false - ); - } else { - finalize(); - } - }; - this._benchmark(message, fn, cb); - } else { - this._benchmark(message, fn, callback); - } - } - - _benchmark(message, fn, cb, log = true) { - setTimeout(() => { - startMeasure(message); - fn(); - stopMeasure(info => { - if (log) { - this.log(info.msg); - } - if (cb) { - cb(info); - } - }); - }, 10); - } - - addMessages(n) { - this.benchmark("add " + n, () => { - const newMessages = buildData(n); - this.state.messages.push.apply(this.state.messages, newMessages); - }); - } - - clear() { - this._benchmark("clear", () => { - this.state.messages = []; - }); - } - - updateSomeMessages() { - this.benchmark("update every 10th", () => { - const messages = this.state.messages; - for (let i = 0; i < this.state.messages.length; i += 10) { - const msg = Object.assign({}, messages[i]); - msg.author += "!!!"; - this.set(messages, i, msg); - } - }); - } - - removeMessage(data) { - this.benchmark("remove message", () => { - const index = this.state.messages.findIndex(m => m.id === data.id); - this.state.messages.splice(index, 1); - }); - } - - log(str, isBold) { - const div = document.createElement("div"); - if (isBold) { - div.classList.add("bold"); - } - div.textContent = `> ${str}`; - this.refs.log.appendChild(div); - this.refs.log.scrollTop = this.refs.log.scrollHeight; - } - - clearLog() { - this.refs.log.innerHTML = ""; - } - - toggleMultiple() { - this.state.multipleFlag = !this.state.multipleFlag; - } - - toggleClear() { - this.state.clearAfterFlag = !this.state.clearAfterFlag; - } -} - -//------------------------------------------------------------------------------ -// Application initialization -//------------------------------------------------------------------------------ -async function start() { - const templates = await owl.utils.loadTemplates("templates.xml"); - const env = { - qweb: new owl.QWeb(templates) - }; - const app = new App(env); - app.mount(document.body); -} - -start(); diff --git a/tools/benchmarks/owl-0.10.0/index.html b/tools/benchmarks/owl-0.10.0/index.html deleted file mode 100644 index 3abd2772..00000000 --- a/tools/benchmarks/owl-0.10.0/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - OWL 0.10.0 Benchmark - - - - -
- - - diff --git a/tools/benchmarks/owl-0.10.0/owl.js b/tools/benchmarks/owl-0.10.0/owl.js deleted file mode 100644 index c59fe751..00000000 --- a/tools/benchmarks/owl-0.10.0/owl.js +++ /dev/null @@ -1,2685 +0,0 @@ -(function (exports) { - 'use strict'; - - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a widget or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let sub of subs) { - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } - - /** - * Owl Observer - * - * This code contains the logic that allows Owl to observe and react to state - * changes. - * - * This is a Observer class that can observe any JS values. The way it works - * can be summarized thusly: - * - primitive values are not observed at all - * - Objects are observed by replacing all their keys with getters/setters - * (recursively) - * - Arrays are observed by replacing their prototype with a customized version, - * which wrap some methods to allow the tracking of each state change. - * - * Note that this code is inspired by Vue. - */ - //------------------------------------------------------------------------------ - // Modified Array prototype - //------------------------------------------------------------------------------ - // we define here a new modified Array prototype, which basically override all - // Array methods that change some state to be able to track their changes - const methodsToPatch = [ - "push", - "pop", - "shift", - "unshift", - "splice", - "sort", - "reverse" - ]; - const ArrayProto = Array.prototype; - const ModifiedArrayProto = Object.create(ArrayProto); - for (let method of methodsToPatch) { - const initialMethod = ArrayProto[method]; - ModifiedArrayProto[method] = function (...args) { - if (!this.__observer__.allowMutations) { - throw new Error(`Array cannot be changed here")`); - } - this.__observer__.notifyChange(); - this.__owl__.rev++; - let parent = this; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent)); - let inserted; - switch (method) { - case "push": - case "unshift": - inserted = args; - break; - case "splice": - inserted = args.slice(2); - break; - } - if (inserted) { - for (let elem of inserted) { - this.__observer__.observe(elem, this); - } - } - return initialMethod.call(this, ...args); - }; - } - //------------------------------------------------------------------------------ - // Observer - //------------------------------------------------------------------------------ - class Observer { - constructor() { - this.rev = 1; - this.allowMutations = true; - this.dirty = false; - } - notifyCB() { } - notifyChange() { - this.rev++; - this.dirty = true; - Promise.resolve().then(() => { - if (this.dirty) { - this.dirty = false; - this.notifyCB(); - } - }); - } - observe(value, parent) { - if (value === null) { - // fun fact: typeof null === 'object' - return; - } - if (typeof value !== "object") { - return; - } - if ("__owl__" in value) { - // already observed - value.__owl__.parent = parent; - return; - } - if (Array.isArray(value)) { - this._observeArr(value, parent); - } - else { - this._observeObj(value, parent); - } - } - set(target, key, value) { - this._addProp(target, key, value); - target.__owl__.rev++; - this.notifyChange(); - } - _observeObj(obj, parent) { - const keys = Object.keys(obj); - obj.__owl__ = { rev: 1, deepRev: 1, parent }; - Object.defineProperty(obj, "__owl__", { enumerable: false }); - for (let key of keys) { - this._addProp(obj, key, obj[key]); - } - } - _observeArr(arr, parent) { - arr.__owl__ = { rev: 1, deepRev: 1, parent }; - Object.defineProperty(arr, "__owl__", { enumerable: false }); - arr.__proto__ = Object.create(ModifiedArrayProto); - arr.__proto__.__observer__ = this; - for (let i = 0; i < arr.length; i++) { - this.observe(arr[i], arr); - } - } - _addProp(obj, key, value) { - var self = this; - Object.defineProperty(obj, key, { - enumerable: true, - get() { - return value; - }, - set(newVal) { - if (newVal !== value) { - if (!self.allowMutations) { - throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); - } - value = newVal; - self.observe(newVal, obj); - obj.__owl__.rev++; - let parent = obj; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent) && parent !== obj); - self.notifyChange(); - } - } - }); - this.observe(value, obj); - } - } - - /** - * Owl VDOM - * - * This file contains an implementation of a virtual DOM, which is a system that - * can generate in-memory representations of a DOM tree, compare them, and - * eventually change a concrete DOM tree to match its representation, in an - * hopefully efficient way. - * - * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our - * needs (see https://github.com/snabbdom/snabbdom). - * - * The main exported values are: - * - interface VNode - * - h function (a helper function to generate a vnode) - * - patch function (to apply a vnode to an actual DOM node) - */ - function vnode(sel, data, children, text, elm) { - let key = data === undefined ? undefined : data.key; - return { - sel: sel, - data: data, - children: children, - text: text, - elm: elm, - key: key - }; - } - //------------------------------------------------------------------------------ - // snabbdom.ts - //------------------------------------------------------------------------------ - function isUndef(s) { - return s === undefined; - } - function isDef(s) { - return s !== undefined; - } - const emptyNode = vnode("", {}, [], undefined, undefined); - function sameVnode(vnode1, vnode2) { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; - } - function isVnode(vnode) { - return vnode.sel !== undefined; - } - function createKeyToOldIdx(children, beginIdx, endIdx) { - let i, map = {}, key, ch; - for (i = beginIdx; i <= endIdx; ++i) { - ch = children[i]; - if (ch != null) { - key = ch.key; - if (key !== undefined) - map[key] = i; - } - } - return map; - } - const hooks = [ - "create", - "update", - "remove", - "destroy", - "pre", - "post" - ]; - function init(modules, domApi) { - let i, j, cbs = {}; - const api = domApi !== undefined ? domApi : htmlDomApi; - for (i = 0; i < hooks.length; ++i) { - cbs[hooks[i]] = []; - for (j = 0; j < modules.length; ++j) { - const hook = modules[j][hooks[i]]; - if (hook !== undefined) { - cbs[hooks[i]].push(hook); - } - } - } - function emptyNodeAt(elm) { - const id = elm.id ? "#" + elm.id : ""; - const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; - return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - function createRmCb(childElm, listeners) { - return function rmCb() { - if (--listeners === 0) { - const parent = api.parentNode(childElm); - api.removeChild(parent, childElm); - } - }; - } - function createElm(vnode, insertedVnodeQueue) { - let i, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.init))) { - i(vnode); - data = vnode.data; - } - } - let children = vnode.children, sel = vnode.sel; - if (sel === "!") { - if (isUndef(vnode.text)) { - vnode.text = ""; - } - vnode.elm = api.createComment(vnode.text); - } - else if (sel !== undefined) { - // Parse selector - const hashIdx = sel.indexOf("#"); - const dotIdx = sel.indexOf(".", hashIdx); - const hash = hashIdx > 0 ? hashIdx : sel.length; - const dot = dotIdx > 0 ? dotIdx : sel.length; - const tag = hashIdx !== -1 || dotIdx !== -1 - ? sel.slice(0, Math.min(hash, dot)) - : sel; - const elm = (vnode.elm = - isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, tag) - : api.createElement(tag)); - if (hash < dot) - elm.setAttribute("id", sel.slice(hash + 1, dot)); - if (dotIdx > 0) - elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); - for (i = 0; i < cbs.create.length; ++i) - cbs.create[i](emptyNode, vnode); - if (array(children)) { - for (i = 0; i < children.length; ++i) { - const ch = children[i]; - if (ch != null) { - api.appendChild(elm, createElm(ch, insertedVnodeQueue)); - } - } - } - else if (primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) - i.create(emptyNode, vnode); - if (i.insert) - insertedVnodeQueue.push(vnode); - } - } - else { - vnode.elm = api.createTextNode(vnode.text); - } - return vnode.elm; - } - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - if (ch != null) { - api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); - } - } - } - function invokeDestroyHook(vnode) { - let i, j, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.destroy))) - i(vnode); - for (i = 0; i < cbs.destroy.length; ++i) - cbs.destroy[i](vnode); - if (vnode.children !== undefined) { - for (j = 0; j < vnode.children.length; ++j) { - i = vnode.children[j]; - if (i != null && typeof i !== "string") { - invokeDestroyHook(i); - } - } - } - } - } - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - let i, listeners, rm, ch = vnodes[startIdx]; - if (ch != null) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (i = 0; i < cbs.remove.length; ++i) - cbs.remove[i](ch, rm); - if (isDef((i = ch.data)) && - isDef((i = i.hook)) && - isDef((i = i.remove))) { - i(ch, rm); - } - else { - rm(); - } - } - else { - // Text node - api.removeChild(parentElm, ch.elm); - } - } - } - } - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - let oldStartIdx = 0, newStartIdx = 0; - let oldEndIdx = oldCh.length - 1; - let oldStartVnode = oldCh[0]; - let oldEndVnode = oldCh[oldEndIdx]; - let newEndIdx = newCh.length - 1; - let newStartVnode = newCh[0]; - let newEndVnode = newCh[newEndIdx]; - let oldKeyToIdx; - let idxInOld; - let elmToMove; - let before; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left - } - else if (oldEndVnode == null) { - oldEndVnode = oldCh[--oldEndIdx]; - } - else if (newStartVnode == null) { - newStartVnode = newCh[++newStartIdx]; - } - else if (newEndVnode == null) { - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newEndVnode)) { - // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldEndVnode, newStartVnode)) { - // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { - // New element - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - else { - elmToMove = oldCh[idxInOld]; - if (elmToMove.sel !== newStartVnode.sel) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } - else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - } - newStartVnode = newCh[++newStartIdx]; - } - } - } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } - else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - } - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - let i, hook; - if (isDef((i = vnode.data)) && - isDef((hook = i.hook)) && - isDef((i = hook.prepatch))) { - i(oldVnode, vnode); - } - const elm = (vnode.elm = oldVnode.elm); - let oldCh = oldVnode.children; - let ch = vnode.children; - if (oldVnode === vnode) - return; - if (vnode.data !== undefined) { - for (i = 0; i < cbs.update.length; ++i) - cbs.update[i](oldVnode, vnode); - i = vnode.data.hook; - if (isDef(i) && isDef((i = i.update))) - i(oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) - updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } - else if (isDef(ch)) { - if (isDef(oldVnode.text)) - api.setTextContent(elm, ""); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } - else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ""); - } - } - else if (oldVnode.text !== vnode.text) { - if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - api.setTextContent(elm, vnode.text); - } - if (isDef(hook) && isDef((i = hook.postpatch))) { - i(oldVnode, vnode); - } - } - return function patch(oldVnode, vnode) { - let i, elm, parent; - const insertedVnodeQueue = []; - for (i = 0; i < cbs.pre.length; ++i) - cbs.pre[i](); - if (!isVnode(oldVnode)) { - oldVnode = emptyNodeAt(oldVnode); - } - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } - else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - createElm(vnode, insertedVnodeQueue); - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - for (i = 0; i < insertedVnodeQueue.length; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0; i < cbs.post.length; ++i) - cbs.post[i](); - return vnode; - }; - } - //------------------------------------------------------------------------------ - // is.ts - //------------------------------------------------------------------------------ - const array = Array.isArray; - function primitive(s) { - return typeof s === "string" || typeof s === "number"; - } - function createElement(tagName) { - return document.createElement(tagName); - } - function createElementNS(namespaceURI, qualifiedName) { - return document.createElementNS(namespaceURI, qualifiedName); - } - function createTextNode(text) { - return document.createTextNode(text); - } - function createComment(text) { - return document.createComment(text); - } - function insertBefore(parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); - } - function removeChild(node, child) { - node.removeChild(child); - } - function appendChild(node, child) { - node.appendChild(child); - } - function parentNode(node) { - return node.parentNode; - } - function nextSibling(node) { - return node.nextSibling; - } - function tagName(elm) { - return elm.tagName; - } - function setTextContent(node, text) { - node.textContent = text; - } - function getTextContent(node) { - return node.textContent; - } - function isElement(node) { - return node.nodeType === 1; - } - function isText(node) { - return node.nodeType === 3; - } - function isComment(node) { - return node.nodeType === 8; - } - const htmlDomApi = { - createElement, - createElementNS, - createTextNode, - createComment, - insertBefore, - removeChild, - appendChild, - parentNode, - nextSibling, - tagName, - setTextContent, - getTextContent, - isElement, - isText, - isComment - }; - function addNS(data, children, sel) { - data.ns = "http://www.w3.org/2000/svg"; - if (sel !== "foreignObject" && children !== undefined) { - for (let i = 0; i < children.length; ++i) { - let childData = children[i].data; - if (childData !== undefined) { - addNS(childData, children[i].children, children[i].sel); - } - } - } - } - function h(sel, b, c) { - var data = {}, children, text, i; - if (c !== undefined) { - data = b; - if (array(c)) { - children = c; - } - else if (primitive(c)) { - text = c; - } - else if (c && c.sel) { - children = [c]; - } - } - else if (b !== undefined) { - if (array(b)) { - children = b; - } - else if (primitive(b)) { - text = b; - } - else if (b && b.sel) { - children = [b]; - } - else { - data = b; - } - } - if (children !== undefined) { - for (i = 0; i < children.length; ++i) { - if (primitive(children[i])) - children[i] = vnode(undefined, undefined, undefined, children[i], undefined); - } - } - if (sel[0] === "s" && - sel[1] === "v" && - sel[2] === "g" && - (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { - addNS(data, children, sel); - } - return vnode(sel, data, children, text, undefined); - } - function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; - if (!oldProps && !props) - return; - if (oldProps === props) - return; - oldProps = oldProps || {}; - props = props || {}; - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - const propsModule = { - create: updateProps, - update: updateProps - }; - function invokeHandler(handler, vnode, event) { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } - else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); - } - else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); - } - } - else { - // call multiple handlers - for (var i = 0; i < handler.length; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } - } - function handleEvent(event, vnode) { - var name = event.type, on = vnode.data.on; - // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } - } - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - oldElm.removeEventListener(name, oldListener, false); - } - } - else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = (vnode.listener = - oldVnode.listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - elm.addEventListener(name, listener, false); - } - } - else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } - } - const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners - }; - const xlinkNS = "http://www.w3.org/1999/xlink"; - const xmlNS = "http://www.w3.org/XML/1998/namespace"; - const colonChar = 58; - const xChar = 120; - function updateAttrs(oldVnode, vnode) { - var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) - return; - if (oldAttrs === attrs) - return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } - else if (cur === false) { - elm.removeAttribute(key); - } - else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } - else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } - else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } - else { - elm.setAttribute(key, cur); - } - } - } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } - } - const attrsModule = { - create: updateAttrs, - update: updateAttrs - }; - const patch = init([eventListenersModule, attrsModule, propsModule]); - - //------------------------------------------------------------------------------ - // Widget - //------------------------------------------------------------------------------ - let nextId = 1; - class Component extends EventBus { - //-------------------------------------------------------------------------- - // Lifecycle - //-------------------------------------------------------------------------- - /** - * Creates an instance of Component. - * - * The root widget of a component tree needs an environment: - * - * ```javascript - * const root = new RootWidget(env, props); - * ``` - * - * Every other widget simply needs a reference to its parent: - * - * ```javascript - * const child = new SomeWidget(parent, props); - * ``` - * - * Note that most of the time, only the root widget needs to be created by - * hand. Other widgets should be created automatically by the framework (with - * the t-widget directive in a template) - */ - constructor(parent, props) { - super(); - this.template = "default"; - this.inlineTemplate = null; - this.refs = {}; - // is this a good idea? - // Pro: if props is empty, we can create easily a widget - // Con: this is not really safe - // Pro: but creating widget (by a template) is always unsafe anyway - this.props = props || {}; - let id = nextId++; - let p = null; - if (parent instanceof Component) { - p = parent; - this.env = parent.env; - parent.__owl__.children[id] = this; - } - else { - this.env = parent; - } - this.__owl__ = { - id: id, - vnode: null, - isStarted: false, - isMounted: false, - isDestroyed: false, - parent: p, - children: {}, - cmap: {}, - renderId: 1, - renderPromise: null, - renderProps: props || null, - boundHandlers: {} - }; - } - get el() { - return this.__owl__.vnode ? this.__owl__.vnode.elm : null; - } - /** - * willStart is an asynchronous hook that can be implemented to perform some - * action before the initial rendering of a component. - * - * It will be called exactly once before the initial rendering. It is useful - * in some cases, for example, to load external assets (such as a JS library) - * before the widget is rendered. - * - * Note that a slow willStart method will slow down the rendering of the user - * interface. Therefore, some effort should be made to make this method as - * fast as possible. - * - * Note: this method should not be called manually. - */ - async willStart() { } - /** - * mounted is a hook that is called each time a component is attached to the - * DOM. This is a good place to add some listeners, or to interact with the - * DOM, if the component needs to perform some measure for example. - * - * Note: this method should not be called manually. - * - * @see willUnmount - */ - mounted() { } - /** - * The willUpdateProps is an asynchronous hook, called just before new props - * are set. This is useful if the component needs some asynchronous task - * performed, depending on the props (for example, assuming that the props are - * some record Id, fetching the record data). - * - * This hook is not called during the first render (but willStart is called - * and performs a similar job). - */ - async willUpdateProps(nextProps) { } - /** - * The willPatch hook is called just before the DOM patching process starts. - * It is not called on the initial render. This is useful to get some - * information which are in the DOM. For example, the current position of the - * scrollbar - * - * The return value of willPatch will be given to the patched function. - */ - willPatch() { } - /** - * This hook is called whenever a component did actually update its props, - * state or env. - * - * This method is not called on the initial render. It is useful to interact - * with the DOM (for example, through an external library) whenever the - * component was updated. - * - * Updating the widget state in this hook is possible, but not encouraged. - * One need to be careful, because updates here will cause rerender, which in - * turn will cause other calls to updated. So, we need to be particularly - * careful at avoiding endless cycles. - * - * The snapshot parameter is the result of the call to willPatch. - */ - patched(snapshot) { } - /** - * willUnmount is a hook that is called each time just before a component is - * unmounted from the DOM. This is a good place to remove some listeners, for - * example. - * - * Note: this method should not be called manually. - * - * @see mounted - */ - willUnmount() { } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - async mount(target) { - const vnode = await this._prepare(); - if (this.__owl__.isDestroyed) { - // widget was destroyed before we get here... - return; - } - this._patch(vnode); - target.appendChild(this.el); - if (document.body.contains(target)) { - this._callMounted(); - } - } - _callMounted() { - const children = this.__owl__.children; - for (let id in children) { - const comp = children[id]; - if (!comp.__owl__.isMounted && this.el.contains(comp.el)) { - comp._callMounted(); - } - } - this.__owl__.isMounted = true; - this.mounted(); - } - _callWillUnmount() { - this.willUnmount(); - this.__owl__.isMounted = false; - const children = this.__owl__.children; - for (let id in children) { - const comp = children[id]; - if (comp.__owl__.isMounted) { - comp._callWillUnmount(); - } - } - } - unmount() { - if (this.__owl__.isMounted) { - this._callWillUnmount(); - this.el.remove(); - } - } - async render(force = false, patchQueue) { - if (!this.__owl__.isMounted) { - return; - } - const shouldPatch = !patchQueue; - if (shouldPatch) { - patchQueue = []; - } - const renderVDom = this._render(force, patchQueue); - const renderId = this.__owl__.renderId; - await renderVDom; - if (shouldPatch && - this.__owl__.isMounted && - renderId === this.__owl__.renderId) { - // we only update the vnode and the actual DOM if no other rendering - // occurred between now and when the render method was initially called. - for (let i = 0; i < patchQueue.length; i++) { - const patch = patchQueue[i]; - patch.push(patch[0].willPatch()); - } - for (let i = 0; i < patchQueue.length; i++) { - const patch = patchQueue[i]; - patch[0]._patch(patch[1]); - } - for (let i = patchQueue.length - 1; i >= 0; i--) { - const patch = patchQueue[i]; - patch[0].patched(patch[2]); - } - } - } - destroy() { - if (!this.__owl__.isDestroyed) { - const el = this.el; - this._destroy(this.__owl__.parent); - if (el) { - el.remove(); - } - } - } - _destroy(parent) { - const isMounted = this.__owl__.isMounted; - if (isMounted) { - this.willUnmount(); - this.__owl__.isMounted = false; - } - const children = Object.values(this.__owl__.children); - for (let child of children) { - child._destroy(this); - } - if (parent) { - let id = this.__owl__.id; - delete parent.__owl__.children[id]; - this.__owl__.parent = null; - } - this.clear(); - this.__owl__.isDestroyed = true; - delete this.__owl__.vnode; - } - shouldUpdate(nextProps) { - return true; - } - /** - * This method is the correct way to update the environment of a widget. Doing - * this will cause a full rerender of the widget and its children, so this is - * an operation that should not be done frequently. - * - * A good usecase for updating the environment would be to update some mostly - * static config keys, such as a boolean to determine if we are in mobile - * mode or not. - */ - async updateEnv(nextEnv) { - if (this.__owl__.parent && this.__owl__.parent.env === this.env) { - this.env = Object.create(this.env); - } - Object.assign(this.env, nextEnv); - if (this.__owl__.isMounted) { - await this.render(true); - } - } - set(target, key, value) { - this.__owl__.observer.set(target, key, value); - } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - async _updateProps(nextProps, forceUpdate = false, patchQueue) { - const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); - if (shouldUpdate) { - await this.willUpdateProps(nextProps); - this.props = nextProps; - await this.render(forceUpdate, patchQueue); - } - } - _patch(vnode) { - this.__owl__.renderPromise = null; - const target = this.__owl__.vnode || document.createElement(vnode.sel); - this.__owl__.vnode = patch(target, vnode); - } - _prepare() { - this.__owl__.renderProps = this.props; - this.__owl__.renderPromise = this._prepareAndRender(); - return this.__owl__.renderPromise; - } - async _prepareAndRender() { - await this.willStart(); - if (this.__owl__.isDestroyed) { - return Promise.resolve(h("div")); - } - this.__owl__.isStarted = true; - if (this.inlineTemplate) { - this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true); - } - this.__owl__.render = this.env.qweb.render.bind(this.env.qweb, this.inlineTemplate || this.template); - this._observeState(); - return this._render(); - } - async _render(force = false, patchQueue = []) { - this.__owl__.renderId++; - const promises = []; - const patch = [this]; - if (this.__owl__.isMounted) { - patchQueue.push(patch); - } - if (this.__owl__.observer) { - this.__owl__.observer.allowMutations = false; - } - let vnode = this.__owl__.render(this, { - promises, - handlers: this.__owl__.boundHandlers, - forceUpdate: force, - patchQueue - }); - patch.push(vnode); - if (this.__owl__.observer) { - this.__owl__.observer.allowMutations = true; - } - // this part is critical for the patching process to be done correctly. The - // tricky part is that a child widget can be rerendered on its own, which - // will update its own vnode representation without the knowledge of the - // parent widget. With this, we make sure that the parent widget will be - // able to patch itself properly after - vnode.key = this.__owl__.id; - this.__owl__.renderProps = this.props; - this.__owl__.renderPromise = Promise.all(promises).then(() => vnode); - return this.__owl__.renderPromise; - } - /** - * Only called by qweb t-widget directive - */ - _mount(vnode, elm) { - this.__owl__.vnode = patch(elm, vnode); - if (this.__owl__.parent && - this.__owl__.parent.__owl__.isMounted && - !this.__owl__.isMounted) { - this._callMounted(); - } - return this.__owl__.vnode; - } - __mount() { - if (!this.__owl__.isMounted) { - this.__owl__.isMounted = true; - this.mounted(); - } - } - _observeState() { - if (this.state) { - this.__owl__.observer = new Observer(); - this.__owl__.observer.observe(this.state); - this.__owl__.observer.notifyCB = this.render.bind(this); - } - } - } - - /** - * Owl Utils - * - * We have here a small collection of utility functions: - * - * - escape - * - memoize - * - debounce - * - patch - * - unpatch - * - loadTemplates - * - loadJS - * - whenReady - * - parseXML - */ - function escape(str) { - if (str === undefined) { - return ""; - } - if (typeof str === "number") { - return String(str); - } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, "'") - .replace(/`/g, "`"); - } - function memoize(f, hash) { - if (!hash) { - hash = args => args.map(a => String(a)).join(","); - } - let cache = {}; - function memoizedFunction(...args) { - let hashValue = hash(args); - if (!(hashValue in cache)) { - cache[hashValue] = f(...args); - } - return cache[hashValue]; - } - return memoizedFunction; - } - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. - * - * Inspired by https://davidwalsh.name/javascript-debounce-function - */ - function debounce(func, wait, immediate) { - let timeout; - return function () { - const context = this; - const args = arguments; - function later() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - } - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - }; - } - function patch$1(C, patchName, patch) { - const proto = C.prototype; - if (!proto.__patches) { - proto.__patches = { - origMethods: {}, - patches: {}, - current: [] - }; - } - if (proto.__patches.patches[patchName]) { - throw new Error(`Patch [${patchName}] already exists`); - } - proto.__patches.patches[patchName] = patch; - applyPatch(proto, patch); - proto.__patches.current.push(patchName); - function applyPatch(proto, patch) { - Object.keys(patch).forEach(function (methodName) { - const method = patch[methodName]; - if (typeof method === "function") { - const original = proto[methodName]; - if (!(methodName in proto.__patches.origMethods)) { - proto.__patches.origMethods[methodName] = original; - } - proto[methodName] = function (...args) { - this._super = original; - return method.call(this, ...args); - }; - } - }); - } - } - function unpatch(C, patchName) { - const proto = C.prototype; - const patchInfo = proto.__patches; - delete proto.__patches; - // reset to original - for (let k in patchInfo.origMethods) { - proto[k] = patchInfo.origMethods[k]; - } - // apply other patches - for (let name of patchInfo.current) { - if (name !== patchName) { - patch$1(C, name, patchInfo.patches[name]); - } - } - } - async function loadTemplates(url) { - const result = await fetch(url); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); - } - let templates = await result.text(); - templates = templates.replace(//g, ""); - return templates; - } - const loadedScripts = {}; - function loadJS(url) { - if (url in loadedScripts) { - return loadedScripts[url]; - } - const promise = new Promise(function (resolve, reject) { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.src = url; - script.onload = function () { - resolve(); - }; - script.onerror = function () { - reject(`Error loading file '${url}'`); - }; - const head = document.head || document.getElementsByTagName("head")[0]; - head.appendChild(script); - }); - loadedScripts[url] = promise; - return promise; - } - function whenReady(fn) { - if (document.readyState === "complete") { - fn(); - } - else { - document.addEventListener("DOMContentLoaded", fn, false); - } - } - function parseXML(xml) { - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, "text/xml"); - if (doc.getElementsByTagName("parsererror").length) { - throw new Error("Invalid XML in template"); - } - return doc; - } - - var _utils = /*#__PURE__*/Object.freeze({ - escape: escape, - memoize: memoize, - debounce: debounce, - patch: patch$1, - unpatch: unpatch, - loadTemplates: loadTemplates, - loadJS: loadJS, - whenReady: whenReady, - parseXML: parseXML - }); - - //------------------------------------------------------------------------------ - // Const/global stuff/helpers - //------------------------------------------------------------------------------ - const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(","); - const WORD_REPLACEMENT = { - and: "&&", - or: "||", - gt: ">", - gte: ">=", - lt: "<", - lte: "<=" - }; - const DISABLED_TAGS = [ - "input", - "textarea", - "button", - "select", - "option", - "optgroup" - ]; - const lineBreakRE = /[\r\n]/; - const whitespaceRE = /\s+/g; - const DIRECTIVE_NAMES = { - name: 1, - att: 1, - attf: 1, - key: 1 - }; - const DIRECTIVES = []; - const UTILS = { - h: h, - objectToAttrString(obj) { - let classes = []; - for (let k in obj) { - if (obj[k]) { - classes.push(k); - } - } - return classes.join(" "); - } - }; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class Context { - constructor(name) { - this.nextID = 1; - this.code = []; - this.variables = {}; - this.definedVariables = {}; - this.escaping = false; - this.parentNode = null; - this.rootNode = null; - this.indentLevel = 0; - this.shouldDefineOwner = false; - this.shouldProtectContext = false; - this.inLoop = false; - this.inPreTag = false; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("var h = this.utils.h;"); - } - generateID() { - const id = this.rootContext.nextID++; - return id; - } - withParent(node) { - if (this === this.rootContext && this.parentNode) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - return this.subContext("parentNode", node); - } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.indentLevel++; - } - dedent() { - this.indentLevel--; - } - addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - closeIf() { - this.dedent(); - this.addLine("}"); - } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - formatExpression(e) { - e = e.trim(); - if (e[0] === "{" && e[e.length - 1] === "}") { - const innerExpr = e - .slice(1, -1) - .split(",") - .map(p => { - let [key, val] = p.trim().split(":"); - if (key === "") { - return ""; - } - if (!val) { - val = key; - } - return `${key}: ${this.formatExpression(val)}`; - }) - .join(","); - return "{" + innerExpr + "}"; - } - // Thanks CHM for this code... - const chars = e.split(""); - let instring = ""; - let invar = ""; - let invarPos = 0; - let r = ""; - chars.push(" "); - for (var i = 0, ilen = chars.length; i < ilen; i++) { - var c = chars[i]; - if (instring.length) { - if (c === instring && chars[i - 1] !== "\\") { - instring = ""; - } - } - else if (c === '"' || c === "'") { - instring = c; - } - else if (c.match(/[a-zA-Z_\$]/) && !invar.length) { - invar = c; - invarPos = i; - continue; - } - else if (c.match(/\W/) && invar.length) { - // TODO: Should check for possible spaces before dot - if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) { - if (!(invar in this.definedVariables)) { - invar = - WORD_REPLACEMENT[invar] || - (invar in this.variables && - this.formatExpression(this.variables[invar])) || - "context['" + invar + "']"; - } - } - r += invar; - invar = ""; - } - else if (invar.length) { - invar += c; - continue; - } - r += c; - } - const result = r.slice(0, -1); - return result; - } - } - //------------------------------------------------------------------------------ - // QWeb rendering engine - //------------------------------------------------------------------------------ - class QWeb { - constructor(data) { - this.templates = {}; - this.utils = UTILS; - if (data) { - this.loadTemplates(data); - } - } - static addDirective(directive) { - DIRECTIVES.push(directive); - DIRECTIVE_NAMES[directive.name] = 1; - DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); - if (directive.extraNames) { - directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1)); - } - } - /** - * Add a template to the internal template map. Note that it is not - * immediately compiled. - */ - addTemplate(name, xmlString, allowDuplicates = false) { - if (name in this.templates && allowDuplicates) { - return; - } - const doc = parseXML(xmlString); - if (!doc.firstChild) { - throw new Error("Invalid template (should not be empty)"); - } - this._addTemplate(name, doc.firstChild); - } - _addTemplate(name, elem) { - if (name in this.templates) { - throw new Error(`Template ${name} already defined`); - } - this._processTemplate(elem); - const template = { - elem, - fn: (context, extra) => { - const compiledFunction = this._compile(name, elem); - template.fn = compiledFunction; - return compiledFunction.call(this, context, extra); - } - }; - this.templates[name] = template; - } - _processTemplate(elem) { - let tbranch = elem.querySelectorAll("[t-elif], [t-else]"); - for (let i = 0, ilen = tbranch.length; i < ilen; i++) { - let node = tbranch[i]; - let prevElem = node.previousElementSibling; - let pattr = function (name) { - return prevElem.getAttribute(name); - }; - let nattr = function (name) { - return +!!node.getAttribute(name); - }; - if (prevElem && (pattr("t-if") || pattr("t-elif"))) { - if (pattr("t-foreach")) { - throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else"); - } - if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) { - return a + b; - }) > 1) { - throw new Error("Only one conditional branching directive is allowed per node"); - } - // All text nodes between branch nodes are removed - let textNode; - while ((textNode = node.previousSibling) !== prevElem) { - if (textNode.nodeValue.trim().length) { - throw new Error("text is not allowed between branching directives"); - } - textNode.remove(); - } - } - else { - throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive"); - } - } - } - /** - * Load templates from a xml (as a string). This will look up for the first - * tag, and will consider each child of this as a template, with - * the name given by the t-name attribute. - */ - loadTemplates(xmlstr) { - const doc = parseXML(xmlstr); - const templates = doc.getElementsByTagName("templates")[0]; - if (!templates) { - return; - } - for (let elem of templates.children) { - const name = elem.getAttribute("t-name"); - this._addTemplate(name, elem); - } - } - /** - * Render a template - * - * @param {string} name the template should already have been added - */ - render(name, context = {}, extra = null) { - const template = this.templates[name]; - if (!template) { - throw new Error(`Template ${name} does not exist`); - } - return template.fn.call(this, context, extra); - } - _compile(name, elem) { - const isDebug = elem.attributes.hasOwnProperty("t-debug"); - const ctx = new Context(name); - this._compileNode(elem, ctx); - if (ctx.shouldProtectContext) { - ctx.code.unshift(" context = Object.create(context);"); - } - if (ctx.shouldDefineOwner) { - // this is necessary to prevent some directives (t-forach for ex) to - // pollute the rendering context by adding some keys in it. - ctx.code.unshift(" let owner = context;"); - } - if (!ctx.rootNode) { - throw new Error("A template should have one root node"); - } - ctx.addLine(`return vn${ctx.rootNode};`); - let template; - try { - template = new Function("context", "extra", ctx.code.join("\n")); - } - catch (e) { - throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`); - } - if (isDebug) { - console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` + - template.toString()); - } - return template; - } - /** - * Generate code from an xml node - * - */ - _compileNode(node, ctx) { - if (!(node instanceof Element)) { - // this is a text node, there are no directive to apply - let text = node.textContent; - if (!ctx.inPreTag) { - if (lineBreakRE.test(text) && !text.trim()) { - return; - } - text = text.replace(whitespaceRE, " "); - } - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); - } - else { - // this is an unusual situation: this text node is the result of the - // template rendering. - let nodeID = ctx.generateID(); - ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); - ctx.rootContext.rootNode = nodeID; - ctx.rootContext.parentNode = nodeID; - } - return; - } - const attributes = node.attributes; - const validDirectives = []; - let withHandlers = false; - // maybe this is not optimal: we iterate on all attributes here, and again - // just after for each directive. - for (let i = 0; i < attributes.length; i++) { - let attrName = attributes[i].name; - if (attrName.startsWith("t-")) { - let dName = attrName.slice(2).split("-")[0]; - if (!(dName in DIRECTIVE_NAMES)) { - throw new Error(`Unknown QWeb directive: '${attrName}'`); - } - } - } - for (let directive of DIRECTIVES) { - let fullName; - let value; - for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; - if (name === "t-" + directive.name || - name.startsWith("t-" + directive.name + "-")) { - fullName = name; - value = attributes[i].textContent; - validDirectives.push({ directive, value, fullName }); - if (directive.name === "on") { - withHandlers = true; - } - } - } - } - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeEncounter) { - const isDone = directive.atNodeEncounter({ - node, - qweb: this, - ctx, - fullName, - value - }); - if (isDone) { - return; - } - } - } - if (node.nodeName !== "t") { - let nodeID = this._compileGenericNode(node, ctx, withHandlers); - ctx = ctx.withParent(nodeID); - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeCreation) { - directive.atNodeCreation({ - node, - qweb: this, - ctx, - fullName, - value, - nodeID - }); - } - } - } - if (node.nodeName === "pre") { - ctx = ctx.subContext("inPreTag", true); - } - this._compileChildren(node, ctx); - for (let { directive, value, fullName } of validDirectives) { - if (directive.finalize) { - directive.finalize({ node, qweb: this, ctx, fullName, value }); - } - } - } - _compileGenericNode(node, ctx, withHandlers = true) { - // nodeType 1 is generic tag - if (node.nodeType !== 1) { - throw new Error("unsupported node type"); - } - const attributes = node.attributes; - const attrs = []; - const props = []; - const tattrs = []; - function handleBooleanProps(key, val) { - let isProp = false; - if (node.nodeName === "input" && key === "checked") { - let type = node.getAttribute("type"); - if (type === "checkbox" || type === "radio") { - isProp = true; - } - } - if (node.nodeName === "option" && key === "selected") { - isProp = true; - } - if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { - isProp = true; - } - if ((key === "readonly" && node.nodeName === "input") || - node.nodeName === "textarea") { - isProp = true; - } - if (isProp) { - props.push(`${key}: _${val}`); - } - } - for (let i = 0; i < attributes.length; i++) { - let name = attributes[i].name; - const value = attributes[i].textContent; - // regular attributes - if (!name.startsWith("t-") && - !node.getAttribute("t-attf-" + name)) { - const attID = ctx.generateID(); - ctx.addLine(`var _${attID} = '${value}';`); - if (!name.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - name = '"' + name + '"'; - } - attrs.push(`${name}: _${attID}`); - handleBooleanProps(name, attID); - } - // dynamic attributes - if (name.startsWith("t-att-")) { - let attName = name.slice(6); - let formattedValue = ctx.formatExpression(ctx.getValue(value)); - if (formattedValue[0] === "{" && - formattedValue[formattedValue.length - 1] === "}") { - formattedValue = `this.utils.objectToAttrString(${formattedValue})`; - } - const attID = ctx.generateID(); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - // we need to combine dynamic with non dynamic attributes: - // class="a" t-att-class="'yop'" should be rendered as class="a yop" - const attValue = node.getAttribute(attName); - if (attValue) { - const attValueID = ctx.generateID(); - ctx.addLine(`var _${attValueID} = ${formattedValue};`); - formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; - const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); - attrs.splice(attrIndex, 1); - } - ctx.addLine(`var _${attID} = ${formattedValue};`); - attrs.push(`${attName}: _${attID}`); - handleBooleanProps(attName, attID); - } - if (name.startsWith("t-attf-")) { - let attName = name.slice(7); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - const formattedExpr = value.replace(/\{\{.*?\}\}/g, s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"); - const attID = ctx.generateID(); - let staticVal = node.getAttribute(attName); - if (staticVal) { - ctx.addLine(`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`); - } - else { - ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`); - } - attrs.push(`${attName}: _${attID}`); - } - // t-att= attributes - if (name === "t-att") { - let id = ctx.generateID(); - ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`); - tattrs.push(id); - } - } - let nodeID = ctx.generateID(); - let nodeKey = node.getAttribute("t-key"); - if (nodeKey) { - nodeKey = ctx.formatExpression(nodeKey); - } - else { - nodeKey = nodeID; - } - const parts = [`key:${nodeKey}`]; - if (attrs.length + tattrs.length > 0) { - parts.push(`attrs:{${attrs.join(",")}}`); - } - if (props.length > 0) { - parts.push(`props:{${props.join(",")}}`); - } - if (withHandlers) { - parts.push(`on:{}`); - } - ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`); - for (let id of tattrs) { - ctx.addIf(`_${id} instanceof Array`); - ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`); - ctx.addElse(); - ctx.addLine(`for (let key in _${id}) {`); - ctx.indent(); - ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`); - ctx.dedent(); - ctx.addLine(`}`); - ctx.closeIf(); - } - ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); - } - return nodeID; - } - _compileChildren(node, ctx) { - if (node.childNodes.length > 0) { - for (let child of Array.from(node.childNodes)) { - this._compileNode(child, ctx); - } - } - } - } - - /** - * Owl QWeb Directives - * - * This file contains the implementation of most standard QWeb directives: - * - t-esc - * - t-raw - * - t-set/t-value - * - t-if/t-elif/t-else - * - t-call - * - t-foreach/t-as - * - t-debug - * - t-log - */ - //------------------------------------------------------------------------------ - // t-esc and t-raw - //------------------------------------------------------------------------------ - UTILS.getFragment = function (str) { - const temp = document.createElement("template"); - temp.innerHTML = str; - return temp.content; - }; - function compileValueNode(value, node, qweb, ctx) { - if (value === "0" && ctx.caller) { - qweb._compileNode(ctx.caller, ctx); - return; - } - if (typeof value === "string") { - let exprID = value; - if (!(value in ctx.definedVariables)) { - exprID = `_${ctx.generateID()}`; - ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`); - } - ctx.addIf(`${exprID} || ${exprID} === 0`); - if (!ctx.parentNode) { - throw new Error("Should not have a text node without a parent"); - } - if (ctx.escaping) { - ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`); - } - else { - let fragID = ctx.generateID(); - ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); - let tempNodeID = ctx.generateID(); - ctx.addLine(`var p${tempNodeID} = {hook: {`); - ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); - ctx.addLine(`}};`); - ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); - ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); - } - if (node.childNodes.length) { - ctx.addElse(); - qweb._compileChildren(node, ctx); - } - ctx.closeIf(); - return; - } - if (value instanceof NodeList) { - for (let node of Array.from(value)) { - qweb._compileNode(node, ctx); - } - } - } - QWeb.addDirective({ - name: "esc", - priority: 70, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-esc")); - compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); - return true; - } - }); - QWeb.addDirective({ - name: "raw", - priority: 80, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-raw")); - compileValueNode(value, node, qweb, ctx); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-set - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "set", - extraNames: ["value"], - priority: 60, - atNodeEncounter({ node, ctx }) { - const variable = node.getAttribute("t-set"); - let value = node.getAttribute("t-value"); - if (value) { - const formattedValue = ctx.formatExpression(value); - if (ctx.variables.hasOwnProperty(variable)) { - ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`); - } - else { - const varName = `_${ctx.generateID()}`; - ctx.addLine(`var ${varName} = ${formattedValue};`); - ctx.definedVariables[varName] = formattedValue; - ctx.variables[variable] = varName; - } - } - else { - ctx.variables[variable] = node.childNodes; - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-if, t-elif, t-else - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "if", - priority: 20, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-if")); - ctx.addIf(`${ctx.formatExpression(cond)}`); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - QWeb.addDirective({ - name: "elif", - priority: 30, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-elif")); - ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - QWeb.addDirective({ - name: "else", - priority: 40, - atNodeEncounter({ ctx }) { - ctx.addLine(`else {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - //------------------------------------------------------------------------------ - // t-call - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "call", - priority: 50, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - throw new Error("Invalid tag for t-call directive (should be 't')"); - } - const subTemplate = node.getAttribute("t-call"); - const nodeTemplate = qweb.templates[subTemplate]; - if (!nodeTemplate) { - throw new Error(`Cannot find template "${subTemplate}" (t-call)`); - } - const nodeCopy = node.cloneNode(true); - nodeCopy.removeAttribute("t-call"); - // extract variables from nodecopy - const tempCtx = new Context(); - tempCtx.nextID = ctx.rootContext.nextID; - qweb._compileNode(nodeCopy, tempCtx); - const vars = Object.assign({}, ctx.variables, tempCtx.variables); - var definedVariables = Object.assign({}, ctx.definedVariables, tempCtx.definedVariables); - ctx.rootContext.nextID = tempCtx.nextID; - // open new scope, if necessary - const hasNewVariables = Object.keys(definedVariables).length > 0; - if (hasNewVariables) { - ctx.addLine("{"); - ctx.indent(); - } - // add new variables, if any - for (let key in definedVariables) { - ctx.addLine(`let ${key} = ${definedVariables[key]}`); - } - // compile sub template - const subCtx = ctx - .subContext("caller", nodeCopy) - .subContext("variables", Object.create(vars)) - .subContext("definedVariables", Object.create(definedVariables)); - qweb._compileNode(nodeTemplate.elem, subCtx); - // close new scope - if (hasNewVariables) { - ctx.dedent(); - ctx.addLine("}"); - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-foreach - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "foreach", - extraNames: ["as"], - priority: 10, - atNodeEncounter({ node, qweb, ctx }) { - ctx.rootContext.shouldProtectContext = true; - ctx = ctx.subContext("inLoop", true); - const elems = node.getAttribute("t-foreach"); - const name = node.getAttribute("t-as"); - let arrayID = ctx.generateID(); - ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); - ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); - ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`); - let keysID = ctx.generateID(); - ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`); - let valuesID = ctx.generateID(); - ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`); - ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`); - ctx.indent(); - ctx.addLine(`context.${name}_first = i === 0;`); - ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`); - ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`); - ctx.addLine(`context.${name}_index = i;`); - ctx.addLine(`context.${name} = _${keysID}[i];`); - ctx.addLine(`context.${name}_value = _${valuesID}[i];`); - const nodeCopy = node.cloneNode(true); - let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); - if (!shouldWarn && node.tagName === "t") { - if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) { - shouldWarn = true; - } - if (!shouldWarn && - node.children.length === 1 && - node.children[0].tagName !== "t" && - !node.children[0].hasAttribute("t-key")) { - shouldWarn = true; - } - } - if (shouldWarn) { - console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`); - } - nodeCopy.removeAttribute("t-foreach"); - qweb._compileNode(nodeCopy, ctx); - ctx.dedent(); - ctx.addLine("}"); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-debug - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "debug", - priority: 99, - atNodeEncounter({ ctx }) { - ctx.addLine("debugger;"); - } - }); - //------------------------------------------------------------------------------ - // t-log - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "log", - priority: 99, - atNodeEncounter({ ctx, value }) { - const expr = ctx.formatExpression(value); - ctx.addLine(`console.log(${expr})`); - } - }); - - /** - * Owl QWeb Extensions - * - * This file contains the implementation of non standard QWeb directives, added - * by Owl and that will only work on Owl projects: - * - * - t-on - * - t-ref - * - t-transition - * - t-widget/t-props/t-keepalive - */ - //------------------------------------------------------------------------------ - // t-on - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "on", - priority: 90, - atNodeCreation({ ctx, fullName, value, nodeID }) { - ctx.rootContext.shouldDefineOwner = true; - const eventName = fullName.slice(5); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handler = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`; - if (extraArgs) { - ctx.addLine(`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`); - } - else { - ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } - } - }); - //------------------------------------------------------------------------------ - // t-ref - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "ref", - priority: 95, - atNodeCreation({ ctx, nodeID, value }) { - const refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`); - ctx.addLine(`p${nodeID}.hook = { - create: (_, n) => context.refs[${refKey}] = n.elm, - };`); - } - }); - //------------------------------------------------------------------------------ - // t-transition - //------------------------------------------------------------------------------ - UTILS.nextFrame = function (cb) { - requestAnimationFrame(() => requestAnimationFrame(cb)); - }; - UTILS.transitionCreate = function (elm, name) { - elm.classList.add(name + "-enter"); - elm.classList.add(name + "-enter-active"); - }; - UTILS.transitionInsert = function (elm, name) { - const finalize = () => { - elm.classList.remove(name + "-enter-active"); - elm.classList.remove(name + "-enter-to"); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-enter"); - elm.classList.add(name + "-enter-to"); - whenTransitionEnd(elm, finalize); - }); - }; - UTILS.transitionRemove = function (elm, name, rm) { - elm.classList.add(name + "-leave"); - elm.classList.add(name + "-leave-active"); - const finalize = () => { - elm.classList.remove(name + "-leave-active"); - elm.classList.remove(name + "-enter-to"); - rm(); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-leave"); - elm.classList.add(name + "-leave-to"); - whenTransitionEnd(elm, finalize); - }); - }; - function getTimeout(delays, durations) { - /* istanbul ignore next */ - while (delays.length < durations.length) { - delays = delays.concat(delays); - } - return Math.max.apply(null, durations.map((d, i) => { - return toMs(d) + toMs(delays[i]); - })); - } - // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers - // in a locale-dependent way, using a comma instead of a dot. - // If comma is not replaced with a dot, the input will be rounded down (i.e. acting - // as a floor function) causing unexpected behaviors - function toMs(s) { - return Number(s.slice(0, -1).replace(",", ".")) * 1000; - } - function whenTransitionEnd(elm, cb) { - const styles = window.getComputedStyle(elm); - const delays = (styles.transitionDelay || "").split(", "); - const durations = (styles.transitionDuration || "").split(", "); - const timeout = getTimeout(delays, durations); - if (timeout > 0) { - elm.addEventListener("transitionend", cb, { once: true }); - } - else { - cb(); - } - } - QWeb.addDirective({ - name: "transition", - priority: 96, - atNodeCreation({ ctx, value, nodeID }) { - let name = value; - ctx.addLine(`p${nodeID}.hook = { - create: (_, n) => { - this.utils.transitionCreate(n.elm, '${name}'); - }, - insert: vn => { - this.utils.transitionInsert(vn.elm, '${name}'); - }, - remove: (vn, rm) => { - this.utils.transitionRemove(vn.elm, '${name}', rm); - } - };`); - } - }); - //------------------------------------------------------------------------------ - // t-widget - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "widget", - extraNames: ["props", "keepalive"], - priority: 100, - atNodeEncounter({ ctx, value, node }) { - ctx.addLine("//WIDGET"); - ctx.rootContext.shouldDefineOwner = true; - let props = node.getAttribute("t-props"); - let keepAlive = node.getAttribute("t-keepalive") ? true : false; - // t-on- events... - const events = []; - const attributes = node.attributes; - for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; - if (name.startsWith("t-on-")) { - events.push([name.slice(5), attributes[i].textContent]); - } - } - let key = node.getAttribute("t-key"); - if (key) { - key = ctx.formatExpression(key); - } - if (props) { - props = ctx.formatExpression(props); - } - let dummyID = ctx.generateID(); - let defID = ctx.generateID(); - let widgetID = ctx.generateID(); - let keyID = key && ctx.generateID(); - if (key) { - // we bind a variable to the key (could be a complex expression, so we - // want to evaluate it only once) - ctx.addLine(`let key${keyID} = ${key};`); - } - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); - ctx.addLine(`c${ctx.parentNode}.push(null);`); - ctx.addLine(`let def${defID};`); - let templateID = key - ? `key${keyID}` - : ctx.inLoop - ? `String(-${widgetID} - i)` - : String(widgetID); - ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); - ctx.addLine(`let props${widgetID} = ${props || "{}"};`); - ctx.addLine(`let isNew${widgetID} = !w${widgetID};`); - // check if we can reuse current rendering promise - ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`); - ctx.addIf(`w${widgetID}.__owl__.isStarted`); - ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`); - ctx.addElse(); - ctx.addLine(`isNew${widgetID} = true`); - ctx.addIf(`props${widgetID} === w${widgetID}.__owl__.renderProps`); - ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`); - ctx.addElse(); - ctx.addLine(`w${widgetID}.destroy();`); - ctx.addLine(`w${widgetID} = false`); - ctx.closeIf(); - ctx.closeIf(); - ctx.closeIf(); - ctx.addIf(`!def${defID}`); - ctx.addIf(`w${widgetID}`); - ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`); - ctx.addElse(); - ctx.addLine(`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`); - ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`); - for (let [event, method] of events) { - ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`); - } - ctx.addLine(`def${defID} = w${widgetID}._prepare();`); - ctx.closeIf(); - ctx.closeIf(); - let ref = node.getAttribute("t-ref"); - let refExpr = ""; - let refKey = ""; - if (ref) { - refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`); - refExpr = `context.refs[${refKey}] = w${widgetID};`; - } - let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}()`; - if (ref) { - finalizeWidgetCode += `;delete context.refs[${refKey}]`; - } - let createHook = ""; - let classAttr = node.getAttribute("class"); - let tattClass = node.getAttribute("t-att-class"); - let styleAttr = node.getAttribute("style"); - let tattStyle = node.getAttribute("t-att-style"); - if (tattStyle) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`); - tattStyle = attVar; - } - let updateClassCode = ""; - if (classAttr || tattClass || styleAttr || tattStyle) { - let classCode = ""; - if (classAttr) { - classCode = - classAttr - .split(" ") - .map(c => `vn.elm.classList.add('${c}')`) - .join(";") + ";"; - } - if (tattClass) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`); - classCode = `for (let k in ${attVar}) { - if (${attVar}[k]) { - vn.elm.classList.add(k); - } - }`; - updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; - } - const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); - const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : ""; - createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`; - } - ctx.addIf(`isNew${widgetID}`); - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}; w${widgetID}.__owl__.pvnode = pvnode;});`); - ctx.addElse(); - ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){${finalizeWidgetCode}}, destroy() {${finalizeWidgetCode}}}}c${ctx.parentNode}[_${dummyID}_index]=vnode;});`); - ctx.closeIf(); - ctx.addLine(`extra.promises.push(def${defID});`); - if (node.hasAttribute("t-if") || node.hasAttribute("t-else")) { - ctx.closeIf(); - } - return true; - } - }); - - class Store extends EventBus { - constructor(config, options = {}) { - super(); - this._commitLevel = 0; - this.history = []; - this.debug = options.debug || false; - this.state = config.state || {}; - this.actions = config.actions; - this.mutations = config.mutations; - this.env = config.env; - this.observer = new Observer(); - this.observer.notifyCB = this.trigger.bind(this, "update"); - this.observer.allowMutations = false; - this.observer.observe(this.state); - this.getters = {}; - if (this.debug) { - this.history.push({ state: this.state }); - } - this.set = this.observer.set.bind(this.observer); - for (let entry of Object.entries(config.getters || {})) { - const name = entry[0]; - const func = entry[1]; - this.getters[name] = payload => { - return func({ state: this.state, getters: this.getters }, payload); - }; - } - } - dispatch(action, payload) { - if (!this.actions[action]) { - throw new Error(`[Error] action ${action} is undefined`); - } - const result = this.actions[action]({ - commit: this.commit.bind(this), - dispatch: this.dispatch.bind(this), - env: this.env, - state: this.state, - getters: this.getters - }, payload); - if (result instanceof Promise) { - return new Promise((resolve, reject) => { - result.then(() => resolve()); - result.catch(reject); - }); - } - } - commit(type, payload) { - if (!this.mutations[type]) { - throw new Error(`[Error] mutation ${type} is undefined`); - } - this._commitLevel++; - this.observer.allowMutations = true; - const res = this.mutations[type].call(null, { - commit: this.commit.bind(this), - state: this.state, - set: this.set, - getters: this.getters - }, payload); - if (this._commitLevel === 1) { - this.observer.allowMutations = false; - if (this.debug) { - this.history.push({ - state: this.state, - mutation: type, - payload: payload - }); - } - } - this._commitLevel--; - return res; - } - } - //------------------------------------------------------------------------------ - // Connect function - //------------------------------------------------------------------------------ - function revNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.rev; - } - return 0; - } - function deepRevNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.deepRev; - } - return 0; - } - function connect(mapStateToProps, options = {}) { - let hashFunction = options.hashFunction || null; - if (!hashFunction) { - let deep = "deep" in options ? options.deep : true; - let defaultRevFunction = deep ? deepRevNumber : revNumber; - hashFunction = function ({ storeProps }, options) { - const { currentStoreProps } = options; - if ("__owl__" in storeProps) { - return defaultRevFunction(storeProps); - } - let hash = 0; - for (let key in storeProps) { - const val = storeProps[key]; - const hashVal = defaultRevFunction(val); - if (hashVal === 0) { - if (val !== currentStoreProps[key]) { - options.didChange = true; - } - } - else { - hash += hashVal; - } - } - return hash; - }; - } - return function (Comp) { - return class extends Comp { - constructor(parent, props) { - const env = parent instanceof Component ? parent.env : parent; - const ownProps = Object.assign({}, props || {}); - const storeProps = mapStateToProps(env.store.state, ownProps, env.store.getters); - const mergedProps = Object.assign({}, props || {}, storeProps); - super(parent, mergedProps); - this.__owl__.ownProps = ownProps; - this.__owl__.currentStoreProps = storeProps; - this.__owl__.storeHash = hashFunction({ - state: env.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, { - currentStoreProps: storeProps - }); - } - /** - * We do not use the mounted hook here for a subtle reason: we want the - * updates to be called for the parents before the children. However, - * if we use the mounted hook, this will be done in the reverse order. - */ - _callMounted() { - this.env.store.on("update", this, this._checkUpdate); - super._callMounted(); - } - willUnmount() { - this.env.store.off("update", this); - super.willUnmount(); - } - _checkUpdate() { - const ownProps = this.__owl__.ownProps; - const storeProps = mapStateToProps(this.env.store.state, ownProps, this.env.store.getters); - const options = { - currentStoreProps: this.__owl__.currentStoreProps - }; - const storeHash = hashFunction({ - state: this.env.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, options); - let didChange = options.didChange; - if (storeHash !== this.__owl__.storeHash) { - didChange = true; - this.__owl__.storeHash = storeHash; - } - if (didChange) { - this.__owl__.currentStoreProps = storeProps; - this._updateProps(ownProps, false); - } - } - _updateProps(nextProps, forceUpdate, patchQueue) { - if (this.__owl__.ownProps !== nextProps) { - this.__owl__.currentStoreProps = mapStateToProps(this.env.store.state, nextProps, this.env.store.getters); - } - this.__owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps); - return super._updateProps(mergedProps, forceUpdate, patchQueue); - } - }; - }; - } - - /** - * This file is the main file packaged by rollup (see rollup.config.js). From - * this file, we export all public owl elements. - * - * Note that dynamic values, such as a date or a commit hash are added by rollup - */ - const utils = _utils; - - exports.utils = utils; - exports.Component = Component; - exports.EventBus = EventBus; - exports.Observer = Observer; - exports.QWeb = QWeb; - exports.connect = connect; - exports.Store = Store; - - exports._version = '0.10.0'; - exports._date = '2019-05-10T21:54:47.545Z'; - exports._hash = 'e04c5b1'; - exports._url = 'https://github.com/odoo/owl'; - -}(this.owl = this.owl || {})); diff --git a/tools/benchmarks/owl-0.10.0/templates.xml b/tools/benchmarks/owl-0.10.0/templates.xml deleted file mode 100644 index 699ebea3..00000000 --- a/tools/benchmarks/owl-0.10.0/templates.xml +++ /dev/null @@ -1,50 +0,0 @@ - -
-
-
Actions
-
- - - - - - -
-
-
- - -
-
- - -
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
- - - -
-
-
- -
- - - - -
- -
- -
- - \ No newline at end of file diff --git a/tools/benchmarks/owl-0.12.0/app.js b/tools/benchmarks/owl-0.12.0/app.js deleted file mode 100644 index 5410383e..00000000 --- a/tools/benchmarks/owl-0.12.0/app.js +++ /dev/null @@ -1,170 +0,0 @@ -import { - buildData, - startMeasure, - stopMeasure, - formatNumber -} from "../shared/utils.js"; - -//------------------------------------------------------------------------------ -// Likes Counter Widget -//------------------------------------------------------------------------------ -class Counter extends owl.Component { - state = { counter: 0 }; - - increment() { - this.state.counter++; - } -} - -//------------------------------------------------------------------------------ -// Message Widget -//------------------------------------------------------------------------------ -class Message extends owl.Component { - widgets = { Counter }; - - shouldUpdate(nextProps) { - return nextProps !== this.props; - } - removeMessage() { - this.trigger("remove_message", { - id: this.props.id - }); - } -} - -//------------------------------------------------------------------------------ -// Root Widget -//------------------------------------------------------------------------------ -class App extends owl.Component { - widgets = { Message }; - state = { messages: [], multipleFlag: false, clearAfterFlag: false }; - - mounted() { - this.log( - `Benchmarking Owl v${owl.__info__.version} (build date: ${ - owl.__info__.date - })` - ); - } - - benchmark(message, fn, callback) { - if (this.state.multipleFlag) { - const N = 20; - let n = N; - let total = 0; - let cb = info => { - let finalize = () => { - n--; - total += info.delta; - if (n === 0) { - const avg = total / N; - this.log(`Average: ${formatNumber(avg)}ms`, true); - if (callback) { - callback(); - } - } else { - this._benchmark(message, fn, cb); - } - }; - - if (this.state.clearAfterFlag) { - this._benchmark( - "clear", - () => { - this.state.messages = []; - }, - finalize, - false - ); - } else { - finalize(); - } - }; - this._benchmark(message, fn, cb); - } else { - this._benchmark(message, fn, callback); - } - } - - _benchmark(message, fn, cb, log = true) { - setTimeout(() => { - startMeasure(message); - fn(); - stopMeasure(info => { - if (log) { - this.log(info.msg); - } - if (cb) { - cb(info); - } - }); - }, 10); - } - - addMessages(n) { - this.benchmark("add " + n, () => { - const newMessages = buildData(n); - this.state.messages.push.apply(this.state.messages, newMessages); - }); - } - - clear() { - this.benchmark("clear", () => { - this.state.messages = []; - }); - } - - updateSomeMessages() { - this.benchmark("update every 10th", () => { - const messages = this.state.messages; - for (let i = 0; i < this.state.messages.length; i += 10) { - const msg = Object.assign({}, messages[i]); - msg.author += "!!!"; - this.set(messages, i, msg); - } - }); - } - - removeMessage(data) { - this.benchmark("remove message", () => { - const index = this.state.messages.findIndex(m => m.id === data.id); - this.state.messages.splice(index, 1); - }); - } - - log(str, isBold) { - const div = document.createElement("div"); - if (isBold) { - div.classList.add("bold"); - } - div.textContent = `> ${str}`; - this.refs.log.appendChild(div); - this.refs.log.scrollTop = this.refs.log.scrollHeight; - } - - clearLog() { - this.refs.log.innerHTML = ""; - } - - toggleMultiple() { - this.state.multipleFlag = !this.state.multipleFlag; - } - - toggleClear() { - this.state.clearAfterFlag = !this.state.clearAfterFlag; - } -} - -//------------------------------------------------------------------------------ -// Application initialization -//------------------------------------------------------------------------------ -async function start() { - const templates = await owl.utils.loadTemplates("templates.xml"); - const env = { - qweb: new owl.QWeb(templates) - }; - const app = new App(env); - app.mount(document.body); -} - -start(); diff --git a/tools/benchmarks/owl-0.12.0/owl.js b/tools/benchmarks/owl-0.12.0/owl.js deleted file mode 100644 index 23514a17..00000000 --- a/tools/benchmarks/owl-0.12.0/owl.js +++ /dev/null @@ -1,3076 +0,0 @@ -(function (exports) { - 'use strict'; - - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a widget or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } - - /** - * Owl Observer - * - * This code contains the logic that allows Owl to observe and react to state - * changes. - * - * This is a Observer class that can observe any JS values. The way it works - * can be summarized thusly: - * - primitive values are not observed at all - * - Objects are observed by replacing all their keys with getters/setters - * (recursively) - * - Arrays are observed by replacing their prototype with a customized version, - * which wrap some methods to allow the tracking of each state change. - * - * Note that this code is inspired by Vue. - */ - //------------------------------------------------------------------------------ - // Modified Array prototype - //------------------------------------------------------------------------------ - // we define here a new modified Array prototype, which basically override all - // Array methods that change some state to be able to track their changes - const methodsToPatch = [ - "push", - "pop", - "shift", - "unshift", - "splice", - "sort", - "reverse" - ]; - const methodLen = methodsToPatch.length; - const ArrayProto = Array.prototype; - const ModifiedArrayProto = Object.create(ArrayProto); - for (let i = 0; i < methodLen; i++) { - const method = methodsToPatch[i]; - const initialMethod = ArrayProto[method]; - ModifiedArrayProto[method] = function (...args) { - if (!this.__observer__.allowMutations) { - throw new Error(`Array cannot be changed here")`); - } - this.__observer__.rev++; - this.__observer__.notifyChange(); - this.__owl__.rev++; - let parent = this; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent)); - let inserted; - switch (method) { - case "push": - case "unshift": - inserted = args; - break; - case "splice": - inserted = args.slice(2); - break; - } - if (inserted) { - for (let i = 0, iLen = inserted.length; i < iLen; i++) { - this.__observer__.observe(inserted[i], this); - } - } - return initialMethod.call(this, ...args); - }; - } - //------------------------------------------------------------------------------ - // Observer - //------------------------------------------------------------------------------ - class Observer { - constructor() { - this.rev = 1; - this.allowMutations = true; - this.dirty = false; - } - notifyCB() { } - notifyChange() { - this.dirty = true; - Promise.resolve().then(() => { - if (this.dirty) { - this.dirty = false; - this.notifyCB(); - } - }); - } - observe(value, parent) { - if (value === null) { - // fun fact: typeof null === 'object' - return; - } - if (typeof value !== "object") { - return; - } - if ("__owl__" in value) { - // already observed - value.__owl__.parent = parent; - return; - } - if (Array.isArray(value)) { - this._observeArr(value, parent); - } - else { - this._observeObj(value, parent); - } - } - set(target, key, value) { - this.rev++; - this._addProp(target, key, value); - target.__owl__.rev++; - this.notifyChange(); - } - _observeObj(obj, parent) { - obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent }; - Object.defineProperty(obj, "__owl__", { enumerable: false }); - for (let key in obj) { - this._addProp(obj, key, obj[key]); - } - } - _observeArr(arr, parent) { - arr.__owl__ = { rev: this.rev, deepRev: this.rev, parent }; - Object.defineProperty(arr, "__owl__", { enumerable: false }); - arr.__proto__ = Object.create(ModifiedArrayProto); - arr.__proto__.__observer__ = this; - for (let i = 0, iLen = arr.length; i < iLen; i++) { - this.observe(arr[i], arr); - } - } - _addProp(obj, key, value) { - var self = this; - Object.defineProperty(obj, key, { - enumerable: true, - get() { - return value; - }, - set(newVal) { - if (newVal !== value) { - self.rev++; - if (!self.allowMutations) { - throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); - } - value = newVal; - self.observe(newVal, obj); - obj.__owl__.rev++; - let parent = obj; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent) && parent !== obj); - self.notifyChange(); - } - } - }); - this.observe(value, obj); - } - } - - /** - * Owl VDOM - * - * This file contains an implementation of a virtual DOM, which is a system that - * can generate in-memory representations of a DOM tree, compare them, and - * eventually change a concrete DOM tree to match its representation, in an - * hopefully efficient way. - * - * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our - * needs (see https://github.com/snabbdom/snabbdom). - * - * The main exported values are: - * - interface VNode - * - h function (a helper function to generate a vnode) - * - patch function (to apply a vnode to an actual DOM node) - */ - function vnode(sel, data, children, text, elm) { - let key = data === undefined ? undefined : data.key; - return { - sel: sel, - data: data, - children: children, - text: text, - elm: elm, - key: key - }; - } - //------------------------------------------------------------------------------ - // snabbdom.ts - //------------------------------------------------------------------------------ - function isUndef(s) { - return s === undefined; - } - function isDef(s) { - return s !== undefined; - } - const emptyNode = vnode("", {}, [], undefined, undefined); - function sameVnode(vnode1, vnode2) { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; - } - function isVnode(vnode) { - return vnode.sel !== undefined; - } - function createKeyToOldIdx(children, beginIdx, endIdx) { - let i, map = {}, key, ch; - for (i = beginIdx; i <= endIdx; ++i) { - ch = children[i]; - if (ch != null) { - key = ch.key; - if (key !== undefined) - map[key] = i; - } - } - return map; - } - const hooks = [ - "create", - "update", - "remove", - "destroy", - "pre", - "post" - ]; - function init(modules, domApi) { - let i, j, cbs = {}; - const api = domApi !== undefined ? domApi : htmlDomApi; - for (i = 0; i < hooks.length; ++i) { - cbs[hooks[i]] = []; - for (j = 0; j < modules.length; ++j) { - const hook = modules[j][hooks[i]]; - if (hook !== undefined) { - cbs[hooks[i]].push(hook); - } - } - } - function emptyNodeAt(elm) { - const id = elm.id ? "#" + elm.id : ""; - const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; - return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - function createRmCb(childElm, listeners) { - return function rmCb() { - if (--listeners === 0) { - const parent = api.parentNode(childElm); - api.removeChild(parent, childElm); - } - }; - } - function createElm(vnode, insertedVnodeQueue) { - let i, iLen, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.init))) { - i(vnode); - data = vnode.data; - } - } - let children = vnode.children, sel = vnode.sel; - if (sel === "!") { - if (isUndef(vnode.text)) { - vnode.text = ""; - } - vnode.elm = api.createComment(vnode.text); - } - else if (sel !== undefined) { - // Parse selector - const hashIdx = sel.indexOf("#"); - const dotIdx = sel.indexOf(".", hashIdx); - const hash = hashIdx > 0 ? hashIdx : sel.length; - const dot = dotIdx > 0 ? dotIdx : sel.length; - const tag = hashIdx !== -1 || dotIdx !== -1 - ? sel.slice(0, Math.min(hash, dot)) - : sel; - const elm = (vnode.elm = - isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, tag) - : api.createElement(tag)); - if (hash < dot) - elm.setAttribute("id", sel.slice(hash + 1, dot)); - if (dotIdx > 0) - elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); - for (i = 0, iLen = cbs.create.length; i < iLen; ++i) - cbs.create[i](emptyNode, vnode); - if (array(children)) { - for (i = 0, iLen = children.length; i < iLen; ++i) { - const ch = children[i]; - if (ch != null) { - api.appendChild(elm, createElm(ch, insertedVnodeQueue)); - } - } - } - else if (primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) - i.create(emptyNode, vnode); - if (i.insert) - insertedVnodeQueue.push(vnode); - } - } - else { - vnode.elm = api.createTextNode(vnode.text); - } - return vnode.elm; - } - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - if (ch != null) { - api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); - } - } - } - function invokeDestroyHook(vnode) { - let i, iLen, j, jLen, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.destroy))) - i(vnode); - for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) - cbs.destroy[i](vnode); - if (vnode.children !== undefined) { - for (j = 0, jLen = vnode.children.length; j < jLen; ++j) { - i = vnode.children[j]; - if (i != null && typeof i !== "string") { - invokeDestroyHook(i); - } - } - } - } - } - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - let i, iLen, listeners, rm, ch = vnodes[startIdx]; - if (ch != null) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) - cbs.remove[i](ch, rm); - if (isDef((i = ch.data)) && - isDef((i = i.hook)) && - isDef((i = i.remove))) { - i(ch, rm); - } - else { - rm(); - } - } - else { - // Text node - api.removeChild(parentElm, ch.elm); - } - } - } - } - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - let oldStartIdx = 0, newStartIdx = 0; - let oldEndIdx = oldCh.length - 1; - let oldStartVnode = oldCh[0]; - let oldEndVnode = oldCh[oldEndIdx]; - let newEndIdx = newCh.length - 1; - let newStartVnode = newCh[0]; - let newEndVnode = newCh[newEndIdx]; - let oldKeyToIdx; - let idxInOld; - let elmToMove; - let before; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left - } - else if (oldEndVnode == null) { - oldEndVnode = oldCh[--oldEndIdx]; - } - else if (newStartVnode == null) { - newStartVnode = newCh[++newStartIdx]; - } - else if (newEndVnode == null) { - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newEndVnode)) { - // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldEndVnode, newStartVnode)) { - // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { - // New element - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - else { - elmToMove = oldCh[idxInOld]; - if (elmToMove.sel !== newStartVnode.sel) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } - else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - } - newStartVnode = newCh[++newStartIdx]; - } - } - } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } - else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - } - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - let i, iLen, hook; - if (isDef((i = vnode.data)) && - isDef((hook = i.hook)) && - isDef((i = hook.prepatch))) { - i(oldVnode, vnode); - } - const elm = (vnode.elm = oldVnode.elm); - let oldCh = oldVnode.children; - let ch = vnode.children; - if (oldVnode === vnode) - return; - if (vnode.data !== undefined) { - for (i = 0, iLen = cbs.update.length; i < iLen; ++i) - cbs.update[i](oldVnode, vnode); - i = vnode.data.hook; - if (isDef(i) && isDef((i = i.update))) - i(oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) - updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } - else if (isDef(ch)) { - if (isDef(oldVnode.text)) - api.setTextContent(elm, ""); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } - else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ""); - } - } - else if (oldVnode.text !== vnode.text) { - if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - api.setTextContent(elm, vnode.text); - } - if (isDef(hook) && isDef((i = hook.postpatch))) { - i(oldVnode, vnode); - } - } - return function patch(oldVnode, vnode) { - let i, iLen, elm, parent; - const insertedVnodeQueue = []; - for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) - cbs.pre[i](); - if (!isVnode(oldVnode)) { - oldVnode = emptyNodeAt(oldVnode); - } - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } - else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - createElm(vnode, insertedVnodeQueue); - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0, iLen = cbs.post.length; i < iLen; ++i) - cbs.post[i](); - return vnode; - }; - } - //------------------------------------------------------------------------------ - // is.ts - //------------------------------------------------------------------------------ - const array = Array.isArray; - function primitive(s) { - return typeof s === "string" || typeof s === "number"; - } - function createElement(tagName) { - return document.createElement(tagName); - } - function createElementNS(namespaceURI, qualifiedName) { - return document.createElementNS(namespaceURI, qualifiedName); - } - function createTextNode(text) { - return document.createTextNode(text); - } - function createComment(text) { - return document.createComment(text); - } - function insertBefore(parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); - } - function removeChild(node, child) { - node.removeChild(child); - } - function appendChild(node, child) { - node.appendChild(child); - } - function parentNode(node) { - return node.parentNode; - } - function nextSibling(node) { - return node.nextSibling; - } - function tagName(elm) { - return elm.tagName; - } - function setTextContent(node, text) { - node.textContent = text; - } - function getTextContent(node) { - return node.textContent; - } - function isElement(node) { - return node.nodeType === 1; - } - function isText(node) { - return node.nodeType === 3; - } - function isComment(node) { - return node.nodeType === 8; - } - const htmlDomApi = { - createElement, - createElementNS, - createTextNode, - createComment, - insertBefore, - removeChild, - appendChild, - parentNode, - nextSibling, - tagName, - setTextContent, - getTextContent, - isElement, - isText, - isComment - }; - function addNS(data, children, sel) { - data.ns = "http://www.w3.org/2000/svg"; - if (sel !== "foreignObject" && children !== undefined) { - for (let i = 0, iLen = children.length; i < iLen; ++i) { - let childData = children[i].data; - if (childData !== undefined) { - addNS(childData, children[i].children, children[i].sel); - } - } - } - } - function h(sel, b, c) { - var data = {}, children, text, i, iLen; - if (c !== undefined) { - data = b; - if (array(c)) { - children = c; - } - else if (primitive(c)) { - text = c; - } - else if (c && c.sel) { - children = [c]; - } - } - else if (b !== undefined) { - if (array(b)) { - children = b; - } - else if (primitive(b)) { - text = b; - } - else if (b && b.sel) { - children = [b]; - } - else { - data = b; - } - } - if (children !== undefined) { - for (i = 0, iLen = children.length; i < iLen; ++i) { - if (primitive(children[i])) - children[i] = vnode(undefined, undefined, undefined, children[i], undefined); - } - } - if (sel[0] === "s" && - sel[1] === "v" && - sel[2] === "g" && - (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { - addNS(data, children, sel); - } - return vnode(sel, data, children, text, undefined); - } - function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; - if (!oldProps && !props) - return; - if (oldProps === props) - return; - oldProps = oldProps || {}; - props = props || {}; - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - const propsModule = { - create: updateProps, - update: updateProps - }; - function invokeHandler(handler, vnode, event) { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } - else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); - } - else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); - } - } - else { - // call multiple handlers - for (let i = 0, iLen = handler.length; i < iLen; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } - } - function handleEvent(event, vnode) { - var name = event.type, on = vnode.data.on; - // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } - } - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - oldElm.removeEventListener(name, oldListener, false); - } - } - else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = (vnode.listener = - oldVnode.listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - elm.addEventListener(name, listener, false); - } - } - else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } - } - const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners - }; - const xlinkNS = "http://www.w3.org/1999/xlink"; - const xmlNS = "http://www.w3.org/XML/1998/namespace"; - const colonChar = 58; - const xChar = 120; - function updateAttrs(oldVnode, vnode) { - var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) - return; - if (oldAttrs === attrs) - return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } - else if (cur === false) { - elm.removeAttribute(key); - } - else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } - else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } - else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } - else { - elm.setAttribute(key, cur); - } - } - } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } - } - const attrsModule = { - create: updateAttrs, - update: updateAttrs - }; - const patch = init([eventListenersModule, attrsModule, propsModule]); - - //------------------------------------------------------------------------------ - // Const/global stuff/helpers - //------------------------------------------------------------------------------ - const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(","); - const WORD_REPLACEMENT = { - and: "&&", - or: "||", - gt: ">", - gte: ">=", - lt: "<", - lte: "<=" - }; - const DISABLED_TAGS = [ - "input", - "textarea", - "button", - "select", - "option", - "optgroup" - ]; - const lineBreakRE = /[\r\n]/; - const whitespaceRE = /\s+/g; - const DIRECTIVE_NAMES = { - name: 1, - att: 1, - attf: 1, - key: 1 - }; - const DIRECTIVES = []; - const NODE_HOOKS_PARAMS = { - create: "(_, n)", - insert: "vn", - remove: "(vn, rm)" - }; - const UTILS = { - h: h, - objectToAttrString(obj) { - let classes = []; - for (let k in obj) { - if (obj[k]) { - classes.push(k); - } - } - return classes.join(" "); - } - }; - function parseXML(xml) { - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, "text/xml"); - if (doc.getElementsByTagName("parsererror").length) { - throw new Error("Invalid XML in template"); - } - return doc; - } - let nextID = 1; - //------------------------------------------------------------------------------ - // QWeb rendering engine - //------------------------------------------------------------------------------ - class QWeb { - constructor(data) { - this.templates = {}; - this.utils = UTILS; - // the id field is useful to be able to hash qweb instances. The current - // use case is that component's templates are qweb dependant, and need to be - // able to map a qweb instance to a template name. - this.id = nextID++; - if (data) { - this.addTemplates(data); - } - this.addTemplate("default", "
"); - } - static addDirective(directive) { - DIRECTIVES.push(directive); - DIRECTIVE_NAMES[directive.name] = 1; - DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); - if (directive.extraNames) { - directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1)); - } - } - static register(name, Component) { - if (QWeb.widgets[name]) { - throw new Error(`Component '${name}' has already been registered`); - } - QWeb.widgets[name] = Component; - } - /** - * Add a template to the internal template map. Note that it is not - * immediately compiled. - */ - addTemplate(name, xmlString) { - const doc = parseXML(xmlString); - if (!doc.firstChild) { - throw new Error("Invalid template (should not be empty)"); - } - this._addTemplate(name, doc.firstChild); - } - /** - * Load templates from a xml (as a string). This will look up for the first - * tag, and will consider each child of this as a template, with - * the name given by the t-name attribute. - */ - addTemplates(xmlstr) { - const doc = parseXML(xmlstr); - const templates = doc.getElementsByTagName("templates")[0]; - if (!templates) { - return; - } - for (let elem of templates.children) { - const name = elem.getAttribute("t-name"); - this._addTemplate(name, elem); - } - } - _addTemplate(name, elem) { - if (name in this.templates) { - throw new Error(`Template ${name} already defined`); - } - this._processTemplate(elem); - const template = { - elem, - fn: (context, extra) => { - const compiledFunction = this._compile(name, elem); - template.fn = compiledFunction; - return compiledFunction.call(this, context, extra); - } - }; - this.templates[name] = template; - } - _processTemplate(elem) { - let tbranch = elem.querySelectorAll("[t-elif], [t-else]"); - for (let i = 0, ilen = tbranch.length; i < ilen; i++) { - let node = tbranch[i]; - let prevElem = node.previousElementSibling; - let pattr = function (name) { - return prevElem.getAttribute(name); - }; - let nattr = function (name) { - return +!!node.getAttribute(name); - }; - if (prevElem && (pattr("t-if") || pattr("t-elif"))) { - if (pattr("t-foreach")) { - throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else"); - } - if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) { - return a + b; - }) > 1) { - throw new Error("Only one conditional branching directive is allowed per node"); - } - // All text nodes between branch nodes are removed - let textNode; - while ((textNode = node.previousSibling) !== prevElem) { - if (textNode.nodeValue.trim().length) { - throw new Error("text is not allowed between branching directives"); - } - textNode.remove(); - } - } - else { - throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive"); - } - } - } - /** - * Render a template - * - * @param {string} name the template should already have been added - */ - render(name, context = {}, extra = null) { - const template = this.templates[name]; - if (!template) { - throw new Error(`Template ${name} does not exist`); - } - return template.fn.call(this, context, extra); - } - _compile(name, elem) { - const isDebug = elem.attributes.hasOwnProperty("t-debug"); - const ctx = new Context(name); - this._compileNode(elem, ctx); - if (ctx.shouldProtectContext) { - ctx.code.unshift(" context = Object.create(context);"); - } - if (ctx.shouldDefineOwner) { - // this is necessary to prevent some directives (t-forach for ex) to - // pollute the rendering context by adding some keys in it. - ctx.code.unshift(" let owner = context;"); - } - if (ctx.shouldDefineQWeb) { - ctx.code.unshift(" let QWeb = this.constructor;"); - } - if (ctx.shouldDefineUtils) { - ctx.code.unshift(" let utils = this.utils;"); - } - if (!ctx.rootNode) { - throw new Error("A template should have one root node"); - } - ctx.addLine(`return vn${ctx.rootNode};`); - let template; - try { - template = new Function("context", "extra", ctx.code.join("\n")); - } - catch (e) { - throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`); - } - if (isDebug) { - console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` + - template.toString()); - } - return template; - } - /** - * Generate code from an xml node - * - */ - _compileNode(node, ctx) { - if (!(node instanceof Element)) { - // this is a text node, there are no directive to apply - let text = node.textContent; - if (!ctx.inPreTag) { - if (lineBreakRE.test(text) && !text.trim()) { - return; - } - text = text.replace(whitespaceRE, " "); - } - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); - } - else { - // this is an unusual situation: this text node is the result of the - // template rendering. - let nodeID = ctx.generateID(); - ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); - ctx.rootContext.rootNode = nodeID; - ctx.rootContext.parentNode = nodeID; - } - return; - } - const attributes = node.attributes; - const validDirectives = []; - let withHandlers = false; - // maybe this is not optimal: we iterate on all attributes here, and again - // just after for each directive. - for (let i = 0; i < attributes.length; i++) { - let attrName = attributes[i].name; - if (attrName.startsWith("t-")) { - let dName = attrName.slice(2).split("-")[0]; - if (!(dName in DIRECTIVE_NAMES)) { - throw new Error(`Unknown QWeb directive: '${attrName}'`); - } - } - } - const DIR_N = DIRECTIVES.length; - const ATTR_N = attributes.length; - for (let i = 0; i < DIR_N; i++) { - let directive = DIRECTIVES[i]; - let fullName; - let value; - for (let j = 0; j < ATTR_N; j++) { - const name = attributes[j].name; - if (name === "t-" + directive.name || - name.startsWith("t-" + directive.name + "-")) { - fullName = name; - value = attributes[j].textContent; - validDirectives.push({ directive, value, fullName }); - if (directive.name === "on") { - withHandlers = true; - } - } - } - } - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeEncounter) { - const isDone = directive.atNodeEncounter({ - node, - qweb: this, - ctx, - fullName, - value - }); - if (isDone) { - return; - } - } - } - if (node.nodeName !== "t") { - 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); - }; - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeCreation) { - directive.atNodeCreation({ - node, - qweb: this, - ctx, - fullName, - value, - nodeID, - addNodeHook - }); - } - } - if (Object.keys(nodeHooks).length) { - ctx.addLine(`p${nodeID}.hook = {`); - for (let hook in nodeHooks) { - ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`); - for (let handler of nodeHooks[hook]) { - ctx.addLine(` ${handler}`); - } - ctx.addLine(` },`); - } - ctx.addLine(`};`); - } - } - if (node.nodeName === "pre") { - ctx = ctx.subContext("inPreTag", true); - } - this._compileChildren(node, ctx); - for (let { directive, value, fullName } of validDirectives) { - if (directive.finalize) { - directive.finalize({ node, qweb: this, ctx, fullName, value }); - } - } - } - _compileGenericNode(node, ctx, withHandlers = true) { - // nodeType 1 is generic tag - if (node.nodeType !== 1) { - throw new Error("unsupported node type"); - } - const attributes = node.attributes; - const attrs = []; - const props = []; - const tattrs = []; - function handleBooleanProps(key, val) { - let isProp = false; - if (node.nodeName === "input" && key === "checked") { - let type = node.getAttribute("type"); - if (type === "checkbox" || type === "radio") { - isProp = true; - } - } - if (node.nodeName === "option" && key === "selected") { - isProp = true; - } - if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { - isProp = true; - } - if ((key === "readonly" && node.nodeName === "input") || - node.nodeName === "textarea") { - isProp = true; - } - if (isProp) { - props.push(`${key}: _${val}`); - } - } - for (let i = 0; i < attributes.length; i++) { - let name = attributes[i].name; - const value = attributes[i].textContent; - // regular attributes - if (!name.startsWith("t-") && - !node.getAttribute("t-attf-" + name)) { - const attID = ctx.generateID(); - ctx.addLine(`var _${attID} = '${value}';`); - if (!name.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - name = '"' + name + '"'; - } - attrs.push(`${name}: _${attID}`); - handleBooleanProps(name, attID); - } - // dynamic attributes - if (name.startsWith("t-att-")) { - let attName = name.slice(6); - let formattedValue = ctx.formatExpression(ctx.getValue(value)); - if (formattedValue[0] === "{" && - formattedValue[formattedValue.length - 1] === "}") { - formattedValue = `this.utils.objectToAttrString(${formattedValue})`; - } - const attID = ctx.generateID(); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - // we need to combine dynamic with non dynamic attributes: - // class="a" t-att-class="'yop'" should be rendered as class="a yop" - const attValue = node.getAttribute(attName); - if (attValue) { - const attValueID = ctx.generateID(); - ctx.addLine(`var _${attValueID} = ${formattedValue};`); - formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; - const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); - attrs.splice(attrIndex, 1); - } - ctx.addLine(`var _${attID} = ${formattedValue};`); - attrs.push(`${attName}: _${attID}`); - handleBooleanProps(attName, attID); - } - if (name.startsWith("t-attf-")) { - let attName = name.slice(7); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - const formattedExpr = ctx.interpolate(value); - const attID = ctx.generateID(); - let staticVal = node.getAttribute(attName); - if (staticVal) { - ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`); - } - else { - ctx.addLine(`var _${attID} = ${formattedExpr};`); - } - attrs.push(`${attName}: _${attID}`); - } - // t-att= attributes - if (name === "t-att") { - let id = ctx.generateID(); - ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`); - tattrs.push(id); - } - } - let nodeID = ctx.generateID(); - let nodeKey = node.getAttribute("t-key"); - if (nodeKey) { - nodeKey = ctx.formatExpression(nodeKey); - } - else { - nodeKey = nodeID; - } - const parts = [`key:${nodeKey}`]; - if (attrs.length + tattrs.length > 0) { - parts.push(`attrs:{${attrs.join(",")}}`); - } - if (props.length > 0) { - parts.push(`props:{${props.join(",")}}`); - } - if (withHandlers) { - parts.push(`on:{}`); - } - ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`); - for (let id of tattrs) { - ctx.addIf(`_${id} instanceof Array`); - ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`); - ctx.addElse(); - ctx.addLine(`for (let key in _${id}) {`); - ctx.indent(); - ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`); - ctx.dedent(); - ctx.addLine(`}`); - ctx.closeIf(); - } - ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); - } - return nodeID; - } - _compileChildren(node, ctx) { - if (node.childNodes.length > 0) { - for (let child of Array.from(node.childNodes)) { - this._compileNode(child, ctx); - } - } - } - } - QWeb.widgets = Object.create(null); - // dev mode enables better error messages or more costly validations - QWeb.dev = false; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class Context { - constructor(name) { - this.nextID = 1; - this.code = []; - this.variables = {}; - this.definedVariables = {}; - this.escaping = false; - this.parentNode = null; - this.rootNode = null; - this.indentLevel = 0; - this.shouldDefineOwner = false; - this.shouldDefineQWeb = false; - this.shouldDefineUtils = false; - this.shouldProtectContext = false; - this.inLoop = false; - this.inPreTag = false; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("var h = this.utils.h;"); - } - generateID() { - const id = this.rootContext.nextID++; - return id; - } - withParent(node) { - if (this === this.rootContext && this.parentNode) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - return this.subContext("parentNode", node); - } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.indentLevel++; - } - dedent() { - this.indentLevel--; - } - addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - closeIf() { - this.dedent(); - this.addLine("}"); - } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - formatExpression(e) { - e = e.trim(); - if (e[0] === "{" && e[e.length - 1] === "}") { - const innerExpr = e - .slice(1, -1) - .split(",") - .map(p => { - let [key, val] = p.trim().split(":"); - if (key === "") { - return ""; - } - if (!val) { - val = key; - } - return `${key}: ${this.formatExpression(val)}`; - }) - .join(","); - return "{" + innerExpr + "}"; - } - // Thanks CHM for this code... - const chars = e.split(""); - let instring = ""; - let invar = ""; - let invarPos = 0; - let r = ""; - chars.push(" "); - for (let i = 0, ilen = chars.length; i < ilen; i++) { - let c = chars[i]; - if (instring.length) { - if (c === instring && chars[i - 1] !== "\\") { - instring = ""; - } - } - else if (c === '"' || c === "'") { - instring = c; - } - else if (c.match(/[a-zA-Z_\$]/) && !invar.length) { - invar = c; - invarPos = i; - continue; - } - else if (c.match(/\W/) && invar.length) { - // TODO: Should check for possible spaces before dot - if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) { - if (!(invar in this.definedVariables)) { - invar = - WORD_REPLACEMENT[invar] || - (invar in this.variables && - this.formatExpression(this.variables[invar])) || - "context['" + invar + "']"; - } - } - r += invar; - invar = ""; - } - else if (invar.length) { - invar += c; - continue; - } - r += c; - } - const result = r.slice(0, -1); - return result; - } - /** - * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted). - * For instance: - * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x}}' -> x - */ - interpolate(s) { - let matches = s.match(/\{\{.*?\}\}/g); - if (matches && matches[0].length === s.length) { - return this.formatExpression(s.slice(2, -2)); - } - matches = s.match(/\#\{.*?\}/g); - if (matches && matches[0].length === s.length) { - return this.formatExpression(s.slice(2, -1)); - } - let formatter = expr => "${" + this.formatExpression(expr) + "}"; - let r = s - .replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2))) - .replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1))); - return "`" + r + "`"; - } - } - - // If a component does not define explicitely a template - // key, it needs to find a template with its name (or a parent's). This is - // qweb dependant, so we need a place to store this information indexed by - // qweb instances. - const TEMPLATE_MAP = {}; - //------------------------------------------------------------------------------ - // Widget - //------------------------------------------------------------------------------ - let nextId = 1; - class Component extends EventBus { - //-------------------------------------------------------------------------- - // Lifecycle - //-------------------------------------------------------------------------- - /** - * Creates an instance of Component. - * - * The root widget of a component tree needs an environment: - * - * ```javascript - * const root = new RootWidget(env, props); - * ``` - * - * Every other widget simply needs a reference to its parent: - * - * ```javascript - * const child = new SomeWidget(parent, props); - * ``` - * - * Note that most of the time, only the root widget needs to be created by - * hand. Other widgets should be created automatically by the framework (with - * the t-widget directive in a template) - */ - constructor(parent, props) { - super(); - this.refs = {}; - const defaultProps = this.constructor.defaultProps; - if (defaultProps) { - props = this._applyDefaultProps(props, defaultProps); - } - if (QWeb.dev) { - this._validateProps(props || {}); - } - // is this a good idea? - // Pro: if props is empty, we can create easily a widget - // Con: this is not really safe - // Pro: but creating widget (by a template) is always unsafe anyway - this.props = props || {}; - let id = nextId++; - let p = null; - if (parent instanceof Component) { - p = parent; - this.env = parent.env; - parent.__owl__.children[id] = this; - } - else { - this.env = parent; - } - this.__owl__ = { - id: id, - vnode: null, - isMounted: false, - isDestroyed: false, - parent: p, - children: {}, - cmap: {}, - renderId: 1, - renderPromise: null, - renderProps: props || null, - boundHandlers: {}, - mountedHandlers: {} - }; - } - get el() { - return this.__owl__.vnode ? this.__owl__.vnode.elm : null; - } - /** - * willStart is an asynchronous hook that can be implemented to perform some - * action before the initial rendering of a component. - * - * It will be called exactly once before the initial rendering. It is useful - * in some cases, for example, to load external assets (such as a JS library) - * before the widget is rendered. - * - * Note that a slow willStart method will slow down the rendering of the user - * interface. Therefore, some effort should be made to make this method as - * fast as possible. - * - * Note: this method should not be called manually. - */ - async willStart() { } - /** - * mounted is a hook that is called each time a component is attached to the - * DOM. This is a good place to add some listeners, or to interact with the - * DOM, if the component needs to perform some measure for example. - * - * Note: this method should not be called manually. - * - * @see willUnmount - */ - mounted() { } - /** - * The willUpdateProps is an asynchronous hook, called just before new props - * are set. This is useful if the component needs some asynchronous task - * performed, depending on the props (for example, assuming that the props are - * some record Id, fetching the record data). - * - * This hook is not called during the first render (but willStart is called - * and performs a similar job). - */ - async willUpdateProps(nextProps) { } - /** - * The willPatch hook is called just before the DOM patching process starts. - * It is not called on the initial render. This is useful to get some - * information which are in the DOM. For example, the current position of the - * scrollbar - * - * The return value of willPatch will be given to the patched function. - */ - willPatch() { } - /** - * This hook is called whenever a component did actually update its props, - * state or env. - * - * This method is not called on the initial render. It is useful to interact - * with the DOM (for example, through an external library) whenever the - * component was updated. - * - * Updating the widget state in this hook is possible, but not encouraged. - * One need to be careful, because updates here will cause rerender, which in - * turn will cause other calls to updated. So, we need to be particularly - * careful at avoiding endless cycles. - * - * The snapshot parameter is the result of the call to willPatch. - */ - patched(snapshot) { } - /** - * willUnmount is a hook that is called each time just before a component is - * unmounted from the DOM. This is a good place to remove some listeners, for - * example. - * - * Note: this method should not be called manually. - * - * @see mounted - */ - willUnmount() { } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - async mount(target) { - const vnode = await this._prepare(); - if (this.__owl__.isDestroyed) { - // widget was destroyed before we get here... - return; - } - this._patch(vnode); - target.appendChild(this.el); - if (document.body.contains(target)) { - this._callMounted(); - } - } - _callMounted() { - const __owl__ = this.__owl__; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (!comp.__owl__.isMounted && this.el.contains(comp.el)) { - comp._callMounted(); - } - } - __owl__.isMounted = true; - const handlers = __owl__.mountedHandlers; - for (let key in handlers) { - handlers[key](); - } - this.mounted(); - } - _callWillUnmount() { - this.willUnmount(); - const __owl__ = this.__owl__; - __owl__.isMounted = false; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (comp.__owl__.isMounted) { - comp._callWillUnmount(); - } - } - } - unmount() { - if (this.__owl__.isMounted) { - this._callWillUnmount(); - this.el.remove(); - } - } - async render(force = false, patchQueue) { - const __owl__ = this.__owl__; - if (!__owl__.isMounted) { - return; - } - const shouldPatch = !patchQueue; - if (shouldPatch) { - patchQueue = []; - } - const renderVDom = this._render(force, patchQueue); - const renderId = __owl__.renderId; - await renderVDom; - if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) { - // we only update the vnode and the actual DOM if no other rendering - // occurred between now and when the render method was initially called. - const patchLen = patchQueue.length; - for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch.push(patch[0].willPatch()); - } - for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch[0]._patch(patch[1]); - } - for (let i = patchLen - 1; i >= 0; i--) { - const patch = patchQueue[i]; - patch[0].patched(patch[2]); - } - } - } - destroy() { - const __owl__ = this.__owl__; - if (!__owl__.isDestroyed) { - const el = this.el; - this._destroy(__owl__.parent); - if (el) { - el.remove(); - } - } - } - _destroy(parent) { - const __owl__ = this.__owl__; - const isMounted = __owl__.isMounted; - if (isMounted) { - this.willUnmount(); - __owl__.isMounted = false; - } - const children = __owl__.children; - for (let key in children) { - children[key]._destroy(this); - } - if (parent) { - let id = __owl__.id; - delete parent.__owl__.children[id]; - __owl__.parent = null; - } - this.clear(); - __owl__.isDestroyed = true; - delete __owl__.vnode; - } - shouldUpdate(nextProps) { - return true; - } - /** - * This method is the correct way to update the environment of a widget. Doing - * this will cause a full rerender of the widget and its children, so this is - * an operation that should not be done frequently. - * - * A good usecase for updating the environment would be to update some mostly - * static config keys, such as a boolean to determine if we are in mobile - * mode or not. - */ - async updateEnv(nextEnv) { - const __owl__ = this.__owl__; - if (__owl__.parent && __owl__.parent.env === this.env) { - this.env = Object.create(this.env); - } - Object.assign(this.env, nextEnv); - if (__owl__.isMounted) { - await this.render(true); - } - } - set(target, key, value) { - this.__owl__.observer.set(target, key, value); - } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - async _updateProps(nextProps, forceUpdate = false, patchQueue) { - const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); - if (shouldUpdate) { - const defaultProps = this.constructor.defaultProps; - if (defaultProps) { - nextProps = this._applyDefaultProps(nextProps, defaultProps); - } - if (QWeb.dev) { - this._validateProps(nextProps); - } - await this.willUpdateProps(nextProps); - this.props = nextProps; - await this.render(forceUpdate, patchQueue); - } - } - _patch(vnode) { - const __owl__ = this.__owl__; - __owl__.renderPromise = null; - const target = __owl__.vnode || document.createElement(vnode.sel); - __owl__.vnode = patch(target, vnode); - } - _prepare() { - const __owl__ = this.__owl__; - __owl__.renderProps = this.props; - __owl__.renderPromise = this._prepareAndRender(); - return __owl__.renderPromise; - } - async _prepareAndRender() { - await this.willStart(); - const __owl__ = this.__owl__; - if (__owl__.isDestroyed) { - return Promise.resolve(h("div")); - } - const qweb = this.env.qweb; - if (!this.template) { - let tmap = TEMPLATE_MAP[qweb.id]; - if (!tmap) { - tmap = {}; - TEMPLATE_MAP[qweb.id] = tmap; - } - let p = this.constructor; - let name = p.name; - let template = tmap[name]; - if (template) { - this.template = template; - } - else { - while ((template = p.name) && - !(template in qweb.templates) && - p !== Component) { - p = p.__proto__; - } - if (p === Component) { - this.template = "default"; - } - else { - tmap[name] = template; - this.template = template; - } - } - } - __owl__.render = qweb.render.bind(qweb, this.template); - this._observeState(); - return this._render(); - } - async _render(force = false, patchQueue = []) { - const __owl__ = this.__owl__; - __owl__.renderId++; - const promises = []; - const patch = [this]; - if (__owl__.isMounted) { - patchQueue.push(patch); - } - if (__owl__.observer) { - __owl__.observer.allowMutations = false; - } - let vnode = __owl__.render(this, { - promises, - handlers: __owl__.boundHandlers, - mountedHandlers: __owl__.mountedHandlers, - forceUpdate: force, - patchQueue - }); - patch.push(vnode); - if (__owl__.observer) { - __owl__.observer.allowMutations = true; - } - // this part is critical for the patching process to be done correctly. The - // tricky part is that a child widget can be rerendered on its own, which - // will update its own vnode representation without the knowledge of the - // parent widget. With this, we make sure that the parent widget will be - // able to patch itself properly after - vnode.key = __owl__.id; - __owl__.renderProps = this.props; - __owl__.renderPromise = Promise.all(promises).then(() => vnode); - return __owl__.renderPromise; - } - /** - * Only called by qweb t-widget directive - */ - _mount(vnode, elm) { - const __owl__ = this.__owl__; - __owl__.vnode = patch(elm, vnode); - if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) { - this._callMounted(); - } - return __owl__.vnode; - } - /** - * Only called by qweb t-widget directive (when t-keepalive is set) - */ - _remount() { - const __owl__ = this.__owl__; - if (!__owl__.isMounted) { - __owl__.isMounted = true; - this.mounted(); - } - } - _observeState() { - if (this.state) { - const __owl__ = this.__owl__; - __owl__.observer = new Observer(); - __owl__.observer.observe(this.state); - __owl__.observer.notifyCB = this.render.bind(this); - } - } - /** - * Apply default props (only top level). - * - * Note that this method does not modify in place the props, it returns a new - * prop object - */ - _applyDefaultProps(props, defaultProps) { - props = props ? Object.create(props) : {}; - for (let propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - return props; - } - /** - * Validate the component props (or next props) against the (static) props - * description. This is potentially an expensive operation: it may needs to - * visit recursively the props and all the children to check if they are valid. - * This is why it is only done in 'dev' mode. - */ - _validateProps(props) { - const propsDef = this.constructor.props; - if (propsDef instanceof Array) { - // list of strings (prop names) - for (let i = 0, l = propsDef.length; i < l; i++) { - if (!(propsDef[i] in props)) { - throw new Error(`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`); - } - } - } - else if (propsDef) { - // propsDef is an object now - for (let propName in propsDef) { - if (!(propName in props)) { - if (propsDef[propName] && !propsDef[propName].optional) { - throw new Error(`Missing props '${propName}' (widget '${this.constructor.name}')`); - } - else { - break; - } - } - let isValid = isValidProp(props[propName], propsDef[propName]); - if (!isValid) { - throw new Error(`Props '${propName}' of invalid type in widget '${this.constructor.name}'`); - } - } - } - } - } - //------------------------------------------------------------------------------ - // Prop validation helper - //------------------------------------------------------------------------------ - /** - * Check if an invidual prop value matches its (static) prop definition - */ - function isValidProp(prop, propDef) { - if (typeof propDef === "function") { - // Check if a value is constructed by some Constructor. Note that there is a - // slight abuse of language: we want to consider primitive values as well. - // - // So, even though 1 is not an instance of Number, we want to consider that - // it is valid. - if (typeof prop === "object") { - return prop instanceof propDef; - } - return typeof prop === propDef.name.toLowerCase(); - } - else if (propDef instanceof Array) { - // If this code is executed, this means that we want to check if a prop - // matches at least one of its descriptor. - let result = false; - for (let i = 0, iLen = propDef.length; i < iLen; i++) { - result = result || isValidProp(prop, propDef[i]); - } - return result; - } - // propsDef is an object - let result = isValidProp(prop, propDef.type); - if (propDef.type === Array) { - for (let i = 0, iLen = prop.length; i < iLen; i++) { - result = result && isValidProp(prop[i], propDef.element); - } - } - if (propDef.type === Object) { - const shape = propDef.shape; - for (let key in shape) { - result = result && isValidProp(prop[key], shape[key]); - } - } - return result; - } - - /** - * Owl QWeb Directives - * - * This file contains the implementation of most standard QWeb directives: - * - t-esc - * - t-raw - * - t-set/t-value - * - t-if/t-elif/t-else - * - t-call - * - t-foreach/t-as - * - t-debug - * - t-log - */ - //------------------------------------------------------------------------------ - // t-esc and t-raw - //------------------------------------------------------------------------------ - UTILS.getFragment = function (str) { - const temp = document.createElement("template"); - temp.innerHTML = str; - return temp.content; - }; - function compileValueNode(value, node, qweb, ctx) { - if (value === "0" && ctx.caller) { - qweb._compileNode(ctx.caller, ctx); - return; - } - if (typeof value === "string") { - let exprID = value; - if (!(value in ctx.definedVariables)) { - exprID = `_${ctx.generateID()}`; - ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`); - } - ctx.addIf(`${exprID} || ${exprID} === 0`); - if (!ctx.parentNode) { - throw new Error("Should not have a text node without a parent"); - } - if (ctx.escaping) { - ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`); - } - else { - let fragID = ctx.generateID(); - ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); - let tempNodeID = ctx.generateID(); - ctx.addLine(`var p${tempNodeID} = {hook: {`); - ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); - ctx.addLine(`}};`); - ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); - ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); - } - if (node.childNodes.length) { - ctx.addElse(); - qweb._compileChildren(node, ctx); - } - ctx.closeIf(); - return; - } - if (value instanceof NodeList) { - for (let node of Array.from(value)) { - qweb._compileNode(node, ctx); - } - } - } - QWeb.addDirective({ - name: "esc", - priority: 70, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-esc")); - compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); - return true; - } - }); - QWeb.addDirective({ - name: "raw", - priority: 80, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-raw")); - compileValueNode(value, node, qweb, ctx); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-set - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "set", - extraNames: ["value"], - priority: 60, - atNodeEncounter({ node, ctx }) { - const variable = node.getAttribute("t-set"); - let value = node.getAttribute("t-value"); - if (value) { - const formattedValue = ctx.formatExpression(value); - if (ctx.variables.hasOwnProperty(variable)) { - ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`); - } - else { - const varName = `_${ctx.generateID()}`; - ctx.addLine(`var ${varName} = ${formattedValue};`); - ctx.definedVariables[varName] = formattedValue; - ctx.variables[variable] = varName; - } - } - else { - ctx.variables[variable] = node.childNodes; - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-if, t-elif, t-else - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "if", - priority: 20, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-if")); - ctx.addIf(`${ctx.formatExpression(cond)}`); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - QWeb.addDirective({ - name: "elif", - priority: 30, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-elif")); - ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - QWeb.addDirective({ - name: "else", - priority: 40, - atNodeEncounter({ ctx }) { - ctx.addLine(`else {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - //------------------------------------------------------------------------------ - // t-call - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "call", - priority: 50, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - throw new Error("Invalid tag for t-call directive (should be 't')"); - } - const subTemplate = node.getAttribute("t-call"); - const nodeTemplate = qweb.templates[subTemplate]; - if (!nodeTemplate) { - throw new Error(`Cannot find template "${subTemplate}" (t-call)`); - } - const nodeCopy = node.cloneNode(true); - nodeCopy.removeAttribute("t-call"); - // extract variables from nodecopy - const tempCtx = new Context(); - tempCtx.nextID = ctx.rootContext.nextID; - qweb._compileNode(nodeCopy, tempCtx); - const vars = Object.assign({}, ctx.variables, tempCtx.variables); - var definedVariables = Object.assign({}, ctx.definedVariables, tempCtx.definedVariables); - ctx.rootContext.nextID = tempCtx.nextID; - // open new scope, if necessary - const hasNewVariables = Object.keys(definedVariables).length > 0; - if (hasNewVariables) { - ctx.addLine("{"); - ctx.indent(); - } - // add new variables, if any - for (let key in definedVariables) { - ctx.addLine(`let ${key} = ${definedVariables[key]}`); - } - // compile sub template - const subCtx = ctx - .subContext("caller", nodeCopy) - .subContext("variables", Object.create(vars)) - .subContext("definedVariables", Object.create(definedVariables)); - qweb._compileNode(nodeTemplate.elem, subCtx); - // close new scope - if (hasNewVariables) { - ctx.dedent(); - ctx.addLine("}"); - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-foreach - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "foreach", - extraNames: ["as"], - priority: 10, - atNodeEncounter({ node, qweb, ctx }) { - ctx.rootContext.shouldProtectContext = true; - ctx = ctx.subContext("inLoop", true); - const elems = node.getAttribute("t-foreach"); - const name = node.getAttribute("t-as"); - let arrayID = ctx.generateID(); - ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); - ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); - ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`); - let keysID = ctx.generateID(); - ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`); - ctx.addLine(`var _length${keysID} = _${keysID}.length;`); - let valuesID = ctx.generateID(); - ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`); - ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`); - ctx.indent(); - ctx.addLine(`context.${name}_first = i === 0;`); - ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`); - ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`); - ctx.addLine(`context.${name}_index = i;`); - ctx.addLine(`context.${name} = _${keysID}[i];`); - ctx.addLine(`context.${name}_value = _${valuesID}[i];`); - const nodeCopy = node.cloneNode(true); - let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); - if (!shouldWarn && node.tagName === "t") { - if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) { - shouldWarn = true; - } - if (!shouldWarn && - node.children.length === 1 && - node.children[0].tagName !== "t" && - !node.children[0].hasAttribute("t-key")) { - shouldWarn = true; - } - } - if (shouldWarn) { - console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`); - } - nodeCopy.removeAttribute("t-foreach"); - qweb._compileNode(nodeCopy, ctx); - ctx.dedent(); - ctx.addLine("}"); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-debug - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "debug", - priority: 99, - atNodeEncounter({ ctx }) { - ctx.addLine("debugger;"); - } - }); - //------------------------------------------------------------------------------ - // t-log - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "log", - priority: 99, - atNodeEncounter({ ctx, value }) { - const expr = ctx.formatExpression(value); - ctx.addLine(`console.log(${expr})`); - } - }); - - /** - * Owl QWeb Extensions - * - * This file contains the implementation of non standard QWeb directives, added - * by Owl and that will only work on Owl projects: - * - * - t-on - * - t-ref - * - t-transition - * - t-widget/t-props/t-keepalive - * - t-mounted - */ - //------------------------------------------------------------------------------ - // t-on - //------------------------------------------------------------------------------ - // these are pieces of code that will be injected into the event handler if - // modifiers are specified - const MODS_CODE = { - prevent: "e.preventDefault();", - self: "if (e.target !== this.elm) {return}", - stop: "e.stopPropagation();" - }; - QWeb.addDirective({ - name: "on", - priority: 90, - atNodeCreation({ ctx, fullName, value, nodeID }) { - ctx.rootContext.shouldDefineOwner = true; - const [eventName, ...mods] = fullName.slice(5).split("."); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handlerName = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - ctx.addIf(`!context['${handlerName}']`); - ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`); - ctx.closeIf(); - let params = extraArgs - ? `owner, ${ctx.formatExpression(extraArgs)}` - : "owner"; - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function (mod) { - return MODS_CODE[mod]; - }) - .join(""); - handler += `context['${handlerName}'].call(${params}, e);}`; - } - else { - handler = `context['${handlerName}'].bind(${params})`; - } - if (extraArgs) { - ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); - } - else { - ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } - } - }); - //------------------------------------------------------------------------------ - // t-ref - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "ref", - priority: 95, - atNodeCreation({ ctx, value, addNodeHook }) { - const refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); - addNodeHook("create", `context.refs[${refKey}] = n.elm;`); - } - }); - //------------------------------------------------------------------------------ - // t-transition - //------------------------------------------------------------------------------ - UTILS.nextFrame = function (cb) { - requestAnimationFrame(() => requestAnimationFrame(cb)); - }; - UTILS.transitionInsert = function (elm, name) { - elm.classList.add(name + "-enter"); - elm.classList.add(name + "-enter-active"); - const finalize = () => { - elm.classList.remove(name + "-enter-active"); - elm.classList.remove(name + "-enter-to"); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-enter"); - elm.classList.add(name + "-enter-to"); - whenTransitionEnd(elm, finalize); - }); - }; - UTILS.transitionRemove = function (elm, name, rm) { - elm.classList.add(name + "-leave"); - elm.classList.add(name + "-leave-active"); - const finalize = () => { - elm.classList.remove(name + "-leave-active"); - elm.classList.remove(name + "-leave-to"); - rm(); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-leave"); - elm.classList.add(name + "-leave-to"); - whenTransitionEnd(elm, finalize); - }); - }; - function getTimeout(delays, durations) { - /* istanbul ignore next */ - while (delays.length < durations.length) { - delays = delays.concat(delays); - } - return Math.max.apply(null, durations.map((d, i) => { - return toMs(d) + toMs(delays[i]); - })); - } - // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers - // in a locale-dependent way, using a comma instead of a dot. - // If comma is not replaced with a dot, the input will be rounded down (i.e. acting - // as a floor function) causing unexpected behaviors - function toMs(s) { - return Number(s.slice(0, -1).replace(",", ".")) * 1000; - } - function whenTransitionEnd(elm, cb) { - const styles = window.getComputedStyle(elm); - const delays = (styles.transitionDelay || "").split(", "); - const durations = (styles.transitionDuration || "").split(", "); - const timeout = getTimeout(delays, durations); - if (timeout > 0) { - elm.addEventListener("transitionend", cb, { once: true }); - } - else { - cb(); - } - } - QWeb.addDirective({ - name: "transition", - priority: 96, - atNodeCreation({ value, addNodeHook }) { - let name = value; - const hooks = { - insert: `this.utils.transitionInsert(vn.elm, '${name}');`, - remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);` - }; - for (let hookName in hooks) { - addNodeHook(hookName, hooks[hookName]); - } - } - }); - //------------------------------------------------------------------------------ - // t-widget - //------------------------------------------------------------------------------ - /** - * The t-widget directive is certainly a complicated and hard to maintain piece - * of code. To help you, fellow developer, if you have to maintain it, I offer - * you this advice: Good luck... - * - * Since it is not 'direct' code, but rather code that generates other code, it - * is not easy to understand. To help you, here is a detailed and commented - * explanation of the code generated by the t-widget directive for the following - * situation: - * ```xml - * - * ``` - * - * ```js - * // this is the virtual node representing the parent div - * let c1 = [], p1 = { key: 1 }; - * var vn1 = h("div", p1, c1); - * - * // t-widget directive: we start by evaluating the expression given by t-key: - * let key5 = "somestring"; - * - * // We keep the index of the position of the widget in the closure. We push - * // null to reserve the slot, and will replace it later by the widget vnode, - * // when it will be ready (do not forget that preparing/rendering a widget is - * // asynchronous) - * let _2_index = c1.length; - * c1.push(null); - * - * // def3 is the deferred that will contain later either the new widget - * // creation, or the props update... - * let def3; - * - * // this is kind of tricky: we need here to find if the widget was already - * // created by a previous rendering. This is done by checking the internal - * // `cmap` (children map) of the parent widget: it maps keys to widget ids, - * // and, then, if there is an id, we look into the children list to get the - * // instance - * let w4 = - * key5 in context.__owl__.cmap - * ? context.__owl__.children[context.__owl__.cmap[key5]] - * : false; - * - * // we evaluate here the props given to the component. It is done here to be - * // able to easily reference it later, and also, it might be an expensive - * // computation, so it is certainly better to do it only once - * let props4 = { flag: context["state"].flag }; - * - * // If we have a widget, currently rendering, but not ready yet, and which was - * // rendered with different props, we do not want to wait for it to be ready, - * // then update it. We simply destroy it, and start anew. - * if ( - * w4 && - * w4.__owl__.renderPromise && - * !w4.__owl__.isStarted && - * props4 !== w4.__owl__.renderProps - * ) { - * w4.destroy(); - * w4 = false; - * } - * - * if (!w4) { - * // in this situation, we need to create a new widget. First step is - * // to get a reference to the class, then create an instance with - * // current context as parent, and the props. - * let W4 = context.widgets["child"]; - * if (!W4) { - * throw new Error("Cannot find the definition of widget 'child'"); - * } - * w4 = new W4(owner, props4); - * - * let utils = this.utils; - * - * // Whenever we rerender the parent widget, we need to be sure that we - * // are able to find the widget instance. To do that, we register it to - * // the parent cmap (children map). Note that the 'template' key is - * // used here, since this is what identify the widget from the template - * // perspective. - * context.__owl__.cmap[key5] = w4.__owl__.id; - * - * // _prepare is called, to basically call willStart, then render the - * // widget - * def3 = w4._prepare(); - * - * def3 = def3.then(vnode => { - * // we create here a virtual node for the parent (NOT the widget). This - * // means that the vdom of the parent will be stopped here, and from - * // the parent's perspective, it simply is a vnode with no children. - * // However, it shares the same dom element with the component root - * // vnode. - * let pvnode = h(vnode.sel, { key: key5 }); - * - * // we add hooks to the parent vnode so we can interact with the new - * // widget at the proper time - * pvnode.data.hook = { - * insert(vn) { - * // the _mount method will patch the widget vdom into the elm vn.elm, - * // then call the mounted hooks. However, suprisingly, the snabbdom - * // patch method actually replace the elm by a new elm, so we need - * // to synchronise the pvnode elm with the resulting elm - * let nvn = w4._mount(vnode, vn.elm); - * pvnode.elm = nvn.elm; - * // what follows is only present if there are animations on the widget - * utils.transitionInsert(vn.elm, "fade"); - * }, - * remove() { - * // override with empty function to prevent from removing the node - * // directly. It will be removed when destroy is called anyway, which - * // delays the removal if there are animations. - * }, - * destroy() { - * // if there are animations, we delay the call to destroy on the - * // widget, if not, we call it directly. - * let finalize = () => { - * w4.destroy(); - * }; - * utils.transitionRemove(vn.elm, "fade", finalize); - * } - * }; - * // the pvnode is inserted at the correct position in the div's children - * c1[_2_index] = pvnode; - * - * // we keep here a reference to the parent vnode (representing the - * // widget, so we can reuse it later whenever we update the widget - * w4.__owl__.pvnode = pvnode; - * }); - * } else { - * // this is the 'update' path of the directive. - * // the call to _updateProps is the actual widget update - * def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); - * def3 = def3.then(() => { - * // if widget was destroyed in the meantime, we do nothing (so, this - * // means that the parent's element children list will have a null in - * // the widget's position, which will cause the pvnode to be removed - * // when it is patched. - * if (w4.__owl__.isDestroyed) { - * return; - * } - * // like above, we register the pvnode to the children list, so it - * // will not be patched out of the dom. - * let pvnode = w4.__owl__.pvnode; - * c1[_2_index] = pvnode; - * }); - * } - * - * // we register the deferred here so the parent can coordinate its patch operation - * // with all the children. - * extra.promises.push(def3); - * return vn1; - * ``` - */ - QWeb.addDirective({ - name: "widget", - extraNames: ["props", "keepalive"], - priority: 100, - atNodeEncounter({ ctx, value, node }) { - ctx.addLine("//WIDGET"); - ctx.rootContext.shouldDefineOwner = true; - ctx.rootContext.shouldDefineQWeb = true; - ctx.rootContext.shouldDefineUtils = true; - let props = node.getAttribute("t-props"); - let keepAlive = node.getAttribute("t-keepalive") ? true : false; - // t-on- events and t-transition - const events = []; - let transition = ""; - const attributes = node.attributes; - for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; - if (name.startsWith("t-on-")) { - events.push([name.slice(5), attributes[i].textContent]); - } - else if (name === "t-transition") { - transition = attributes[i].textContent; - } - } - let key = node.getAttribute("t-key"); - if (key) { - key = ctx.formatExpression(key); - } - if (props) { - props = ctx.formatExpression(props); - } - let dummyID = ctx.generateID(); - let defID = ctx.generateID(); - let widgetID = ctx.generateID(); - let keyID = key && ctx.generateID(); - if (key) { - // we bind a variable to the key (could be a complex expression, so we - // want to evaluate it only once) - ctx.addLine(`let key${keyID} = ${key};`); - } - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); - ctx.addLine(`c${ctx.parentNode}.push(null);`); - ctx.addLine(`let def${defID};`); - let templateID = key - ? `key${keyID}` - : ctx.inLoop - ? `String(-${widgetID} - i)` - : String(widgetID); - let ref = node.getAttribute("t-ref"); - let refExpr = ""; - let refKey = ""; - if (ref) { - refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); - refExpr = `context.refs[${refKey}] = w${widgetID};`; - } - let transitionsInsertCode = ""; - if (transition) { - transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`; - } - let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}();`; - if (ref) { - finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true? - } - if (transition) { - finalizeWidgetCode = `let finalize = () => { - ${finalizeWidgetCode} - }; - utils.transitionRemove(vn.elm, '${transition}', finalize);`; - } - let createHook = ""; - let classAttr = node.getAttribute("class"); - let tattClass = node.getAttribute("t-att-class"); - let styleAttr = node.getAttribute("style"); - let tattStyle = node.getAttribute("t-att-style"); - if (tattStyle) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`); - tattStyle = attVar; - } - let updateClassCode = ""; - if (classAttr || tattClass || styleAttr || tattStyle) { - let classCode = ""; - if (classAttr) { - classCode = - classAttr - .split(" ") - .map(c => `vn.elm.classList.add('${c}')`) - .join(";") + ";"; - } - if (tattClass) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`); - classCode = `for (let k in ${attVar}) { - if (${attVar}[k]) { - vn.elm.classList.add(k); - } - }`; - updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; - } - const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); - const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : ""; - createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`; - } - ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); - ctx.addLine(`let props${widgetID} = ${props || "{}"};`); - ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`); - ctx.addLine(`w${widgetID}.destroy();`); - ctx.addLine(`w${widgetID} = false`); - ctx.closeIf(); - ctx.addIf(`!w${widgetID}`); - // new widget - ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`); - ctx.addLine(`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`); - // maybe only do this in dev mode... - ctx.addLine(`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`); - ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`); - ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`); - for (let [event, method] of events) { - ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`); - } - ctx.addLine(`def${defID} = w${widgetID}._prepare();`); - // hack: specify empty remove hook to prevent the node from being removed from the DOM - // FIXME: click to re-add widget during remove transition -> leak - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`); - ctx.addElse(); - // need to update widget - ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`); - let keepAliveCode = ""; - if (keepAlive) { - keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`; - } - ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`); - ctx.closeIf(); - ctx.addLine(`extra.promises.push(def${defID});`); - if (node.hasAttribute("t-if") || - node.hasAttribute("t-else") || - node.hasAttribute("t-elif")) { - ctx.closeIf(); - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-mounted - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "mounted", - priority: 97, - atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) { - ctx.rootContext.shouldDefineOwner = true; - const eventName = fullName.slice(5); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handler = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`; - if (extraArgs) { - ctx.addLine(`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`); - } - else { - ctx.addLine(`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`); - } - addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`); - } - }); - - class Store extends EventBus { - constructor(config, options = {}) { - super(); - this._commitLevel = 0; - this.history = []; - this.debug = options.debug || false; - this.state = config.state || {}; - this.actions = config.actions; - this.mutations = config.mutations; - this.env = config.env; - this.observer = new Observer(); - this.observer.notifyCB = this.trigger.bind(this, "update"); - this.observer.allowMutations = false; - this.observer.observe(this.state); - this.getters = {}; - if (this.debug) { - this.history.push({ state: this.state }); - } - this.set = this.observer.set.bind(this.observer); - for (let entry of Object.entries(config.getters || {})) { - const name = entry[0]; - const func = entry[1]; - this.getters[name] = payload => { - return func({ state: this.state, getters: this.getters }, payload); - }; - } - } - dispatch(action, payload) { - if (!this.actions[action]) { - throw new Error(`[Error] action ${action} is undefined`); - } - const result = this.actions[action]({ - commit: this.commit.bind(this), - dispatch: this.dispatch.bind(this), - env: this.env, - state: this.state, - getters: this.getters - }, payload); - if (result instanceof Promise) { - return new Promise((resolve, reject) => { - result.then(() => resolve()); - result.catch(reject); - }); - } - } - commit(type, payload) { - if (!this.mutations[type]) { - throw new Error(`[Error] mutation ${type} is undefined`); - } - this._commitLevel++; - this.observer.allowMutations = true; - const res = this.mutations[type].call(null, { - commit: this.commit.bind(this), - state: this.state, - set: this.set, - getters: this.getters - }, payload); - if (this._commitLevel === 1) { - this.observer.allowMutations = false; - if (this.debug) { - this.history.push({ - state: this.state, - mutation: type, - payload: payload - }); - } - } - this._commitLevel--; - return res; - } - } - //------------------------------------------------------------------------------ - // Connect function - //------------------------------------------------------------------------------ - function revNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.rev; - } - return 0; - } - function deepRevNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.deepRev; - } - return 0; - } - let nextID$1 = 1; - function connect(mapStateToProps, options = {}) { - let hashFunction = options.hashFunction || null; - if (!hashFunction) { - let deep = "deep" in options ? options.deep : true; - let defaultRevFunction = deep ? deepRevNumber : revNumber; - hashFunction = function ({ storeProps }, options) { - const { currentStoreProps } = options; - if ("__owl__" in storeProps) { - return defaultRevFunction(storeProps); - } - let hash = 0; - for (let key in storeProps) { - const val = storeProps[key]; - const hashVal = defaultRevFunction(val); - if (hashVal === 0) { - if (val !== currentStoreProps[key]) { - options.didChange = true; - } - } - else { - hash += hashVal; - } - } - return hash; - }; - } - return function (Comp) { - const Result = class extends Comp { - constructor(parent, props) { - const env = parent instanceof Component ? parent.env : parent; - const ownProps = Object.assign({}, props || {}); - const storeProps = mapStateToProps(env.store.state, ownProps, env.store.getters); - const mergedProps = Object.assign({}, props || {}, storeProps); - super(parent, mergedProps); - this.__owl__.ownProps = ownProps; - this.__owl__.currentStoreProps = storeProps; - this.__owl__.storeHash = hashFunction({ - state: env.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, { - currentStoreProps: storeProps - }); - } - /** - * We do not use the mounted hook here for a subtle reason: we want the - * updates to be called for the parents before the children. However, - * if we use the mounted hook, this will be done in the reverse order. - */ - _callMounted() { - this.env.store.on("update", this, this._checkUpdate); - super._callMounted(); - } - willUnmount() { - this.env.store.off("update", this); - super.willUnmount(); - } - _checkUpdate() { - const ownProps = this.__owl__.ownProps; - const storeProps = mapStateToProps(this.env.store.state, ownProps, this.env.store.getters); - const options = { - currentStoreProps: this.__owl__.currentStoreProps - }; - const storeHash = hashFunction({ - state: this.env.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, options); - let didChange = options.didChange; - if (storeHash !== this.__owl__.storeHash) { - didChange = true; - this.__owl__.storeHash = storeHash; - } - if (didChange) { - this.__owl__.currentStoreProps = storeProps; - this._updateProps(ownProps, false); - } - } - _updateProps(nextProps, forceUpdate, patchQueue) { - if (this.__owl__.ownProps !== nextProps) { - this.__owl__.currentStoreProps = mapStateToProps(this.env.store.state, nextProps, this.env.store.getters); - } - this.__owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps); - return super._updateProps(mergedProps, forceUpdate, patchQueue); - } - }; - // we assign here a unique name to the resulting anonymous class. - // this is necessary for Owl to be able to properly deduce templates. - // Otherwise, all connected components would have the same name, and then - // each component after the first will necessarily have the same template. - let name = `ConnectedComponent${nextID$1++}`; - Object.defineProperty(Result, "name", { value: name }); - return Result; - }; - } - - /** - * Owl Utils - * - * We have here a small collection of utility functions: - * - * - whenReady - * - loadJS - * - loadTemplates - * - escape - * - debounce - */ - function whenReady(fn) { - if (document.readyState === "complete") { - fn(); - } - else { - document.addEventListener("DOMContentLoaded", fn, false); - } - } - const loadedScripts = {}; - function loadJS(url) { - if (url in loadedScripts) { - return loadedScripts[url]; - } - const promise = new Promise(function (resolve, reject) { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.src = url; - script.onload = function () { - resolve(); - }; - script.onerror = function () { - reject(`Error loading file '${url}'`); - }; - const head = document.head || document.getElementsByTagName("head")[0]; - head.appendChild(script); - }); - loadedScripts[url] = promise; - return promise; - } - async function loadTemplates(url) { - const result = await fetch(url); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); - } - let templates = await result.text(); - templates = templates.replace(//g, ""); - return templates; - } - function escape(str) { - if (str === undefined) { - return ""; - } - if (typeof str === "number") { - return String(str); - } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, "'") - .replace(/`/g, "`"); - } - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. - * - * Inspired by https://davidwalsh.name/javascript-debounce-function - */ - function debounce(func, wait, immediate) { - let timeout; - return function () { - const context = this; - const args = arguments; - function later() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - } - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - }; - } - - var _utils = /*#__PURE__*/Object.freeze({ - whenReady: whenReady, - loadJS: loadJS, - loadTemplates: loadTemplates, - escape: escape, - debounce: debounce - }); - - /** - * This file is the main file packaged by rollup (see rollup.config.js). From - * this file, we export all public owl elements. - * - * Note that dynamic values, such as a date or a commit hash are added by rollup - */ - const __info__ = {}; - Object.defineProperty(__info__, "mode", { - get() { - return QWeb.dev ? "dev" : "prod"; - }, - set(mode) { - QWeb.dev = mode === "dev"; - if (QWeb.dev) { - const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`; - console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`); - } - else { - console.log(`Owl is now running in 'prod' mode.`); - } - } - }); - const utils = _utils; - - exports.Component = Component; - exports.EventBus = EventBus; - exports.Observer = Observer; - exports.QWeb = QWeb; - exports.Store = Store; - exports.__info__ = __info__; - exports.connect = connect; - exports.utils = utils; - - exports.__info__.version = '0.12.0'; - exports.__info__.date = '2019-05-29T11:18:30.894Z'; - exports.__info__.hash = '60cea14'; - exports.__info__.url = 'https://github.com/odoo/owl'; - -}(this.owl = this.owl || {})); diff --git a/tools/benchmarks/owl-0.12.0/templates.xml b/tools/benchmarks/owl-0.12.0/templates.xml deleted file mode 100644 index 2207465d..00000000 --- a/tools/benchmarks/owl-0.12.0/templates.xml +++ /dev/null @@ -1,50 +0,0 @@ - -
-
-
Actions
-
- - - - - - -
-
-
- - -
-
- - -
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
- - - -
-
-
- -
- - - - -
- -
- -
- - \ No newline at end of file diff --git a/tools/benchmarks/owl-0.14.0/index.html b/tools/benchmarks/owl-0.14.0/index.html deleted file mode 100644 index 61cf0b1b..00000000 --- a/tools/benchmarks/owl-0.14.0/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - OWL 0.14.0 Benchmark - - - - -
- - - diff --git a/tools/benchmarks/owl-0.14.0/owl.js b/tools/benchmarks/owl-0.14.0/owl.js deleted file mode 100644 index c65f3baf..00000000 --- a/tools/benchmarks/owl-0.14.0/owl.js +++ /dev/null @@ -1,3410 +0,0 @@ -(function (exports) { - 'use strict'; - - /** - * Owl Observer - * - * This code contains the logic that allows Owl to observe and react to state - * changes. - * - * This is a Observer class that can observe any JS values. The way it works - * can be summarized thusly: - * - primitive values are not observed at all - * - Objects are observed by replacing all their keys with getters/setters - * (recursively) - * - Arrays are observed by replacing their prototype with a customized version, - * which wrap some methods to allow the tracking of each state change. - * - * Note that this code is inspired by Vue. - */ - //------------------------------------------------------------------------------ - // Modified Array prototype - //------------------------------------------------------------------------------ - // we define here a new modified Array prototype, which basically override all - // Array methods that change some state to be able to track their changes - const methodsToPatch = [ - "push", - "pop", - "shift", - "unshift", - "splice", - "sort", - "reverse" - ]; - const methodLen = methodsToPatch.length; - const ArrayProto = Array.prototype; - const ModifiedArrayProto = Object.create(ArrayProto); - for (let i = 0; i < methodLen; i++) { - const method = methodsToPatch[i]; - const initialMethod = ArrayProto[method]; - ModifiedArrayProto[method] = function (...args) { - if (!this.__observer__.allowMutations) { - throw new Error(`Array cannot be changed here")`); - } - this.__observer__.rev++; - this.__observer__.notifyChange(); - this.__owl__.rev++; - let parent = this; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent)); - let inserted; - switch (method) { - case "push": - case "unshift": - inserted = args; - break; - case "splice": - inserted = args.slice(2); - break; - } - if (inserted) { - for (let i = 0, iLen = inserted.length; i < iLen; i++) { - this.__observer__.observe(inserted[i], this); - } - } - return initialMethod.call(this, ...args); - }; - } - //------------------------------------------------------------------------------ - // Observer - //------------------------------------------------------------------------------ - class Observer { - constructor() { - this.rev = 1; - this.allowMutations = true; - this.dirty = false; - } - static set(target, key, value) { - if (!target.__owl__) { - throw Error("`Observer.set()` can only be called with observed Objects or Arrays"); - } - target.__owl__.observer.set(target, key, value); - } - static delete(target, key) { - if (!target.__owl__) { - throw Error("`Observer.delete()` can only be called with observed Objects"); - } - target.__owl__.observer.delete(target, key); - } - notifyCB() { } - notifyChange() { - this.dirty = true; - Promise.resolve().then(() => { - if (this.dirty) { - this.dirty = false; - this.notifyCB(); - } - }); - } - observe(value, parent) { - if (value === null) { - // fun fact: typeof null === 'object' - return; - } - if (typeof value !== "object") { - return; - } - if ("__owl__" in value) { - // already observed - value.__owl__.parent = parent; - return; - } - if (Array.isArray(value)) { - this._observeArr(value, parent); - } - else { - this._observeObj(value, parent); - } - } - set(target, key, value) { - let alreadyDefined = key in target && - Object.getOwnPropertyDescriptor(target, key).configurable === false; - if (alreadyDefined) { - target[key] = value; - } - else { - this._addProp(target, key, value); - this._updateRevNumber(target); - } - this.notifyChange(); - } - delete(target, key) { - delete target[key]; - this._updateRevNumber(target); - this.notifyChange(); - } - _observeObj(obj, parent) { - obj.__owl__ = { - rev: this.rev, - deepRev: this.rev, - parent, - observer: this - }; - Object.defineProperty(obj, "__owl__", { enumerable: false }); - for (let key in obj) { - this._addProp(obj, key, obj[key]); - } - } - _observeArr(arr, parent) { - arr.__owl__ = { - rev: this.rev, - deepRev: this.rev, - parent, - observer: this - }; - Object.defineProperty(arr, "__owl__", { enumerable: false }); - arr.__proto__ = Object.create(ModifiedArrayProto); - arr.__proto__.__observer__ = this; - for (let i = 0, iLen = arr.length; i < iLen; i++) { - this.observe(arr[i], arr); - } - } - _addProp(obj, key, value) { - var self = this; - Object.defineProperty(obj, key, { - configurable: true, - enumerable: true, - get() { - return value; - }, - set(newVal) { - if (newVal !== value) { - if (!self.allowMutations) { - throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); - } - self._updateRevNumber(obj); - value = newVal; - self.observe(newVal, obj); - self.notifyChange(); - } - } - }); - this.observe(value, obj); - } - _updateRevNumber(target) { - this.rev++; - target.__owl__.rev++; - let parent = target; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent) && parent !== target); - } - } - - /** - * Owl VDOM - * - * This file contains an implementation of a virtual DOM, which is a system that - * can generate in-memory representations of a DOM tree, compare them, and - * eventually change a concrete DOM tree to match its representation, in an - * hopefully efficient way. - * - * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our - * needs (see https://github.com/snabbdom/snabbdom). - * - * The main exported values are: - * - interface VNode - * - h function (a helper function to generate a vnode) - * - patch function (to apply a vnode to an actual DOM node) - */ - function vnode(sel, data, children, text, elm) { - let key = data === undefined ? undefined : data.key; - return { sel, data, children, text, elm, key }; - } - //------------------------------------------------------------------------------ - // snabbdom.ts - //------------------------------------------------------------------------------ - function isUndef(s) { - return s === undefined; - } - function isDef(s) { - return s !== undefined; - } - const emptyNode = vnode("", {}, [], undefined, undefined); - function sameVnode(vnode1, vnode2) { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; - } - function isVnode(vnode) { - return vnode.sel !== undefined; - } - function createKeyToOldIdx(children, beginIdx, endIdx) { - let i, map = {}, key, ch; - for (i = beginIdx; i <= endIdx; ++i) { - ch = children[i]; - if (ch != null) { - key = ch.key; - if (key !== undefined) - map[key] = i; - } - } - return map; - } - const hooks = [ - "create", - "update", - "remove", - "destroy", - "pre", - "post" - ]; - function init(modules, domApi) { - let i, j, cbs = {}; - const api = domApi !== undefined ? domApi : htmlDomApi; - for (i = 0; i < hooks.length; ++i) { - cbs[hooks[i]] = []; - for (j = 0; j < modules.length; ++j) { - const hook = modules[j][hooks[i]]; - if (hook !== undefined) { - cbs[hooks[i]].push(hook); - } - } - } - function emptyNodeAt(elm) { - const id = elm.id ? "#" + elm.id : ""; - const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; - return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - function createRmCb(childElm, listeners) { - return function rmCb() { - if (--listeners === 0) { - const parent = api.parentNode(childElm); - api.removeChild(parent, childElm); - } - }; - } - function createElm(vnode, insertedVnodeQueue) { - let i, iLen, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.init))) { - i(vnode); - data = vnode.data; - } - } - let children = vnode.children, sel = vnode.sel; - if (sel === "!") { - if (isUndef(vnode.text)) { - vnode.text = ""; - } - vnode.elm = api.createComment(vnode.text); - } - else if (sel !== undefined) { - // Parse selector - const hashIdx = sel.indexOf("#"); - const dotIdx = sel.indexOf(".", hashIdx); - const hash = hashIdx > 0 ? hashIdx : sel.length; - const dot = dotIdx > 0 ? dotIdx : sel.length; - const tag = hashIdx !== -1 || dotIdx !== -1 - ? sel.slice(0, Math.min(hash, dot)) - : sel; - const elm = (vnode.elm = - isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, tag) - : api.createElement(tag)); - if (hash < dot) - elm.setAttribute("id", sel.slice(hash + 1, dot)); - if (dotIdx > 0) - elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); - for (i = 0, iLen = cbs.create.length; i < iLen; ++i) - cbs.create[i](emptyNode, vnode); - if (array(children)) { - for (i = 0, iLen = children.length; i < iLen; ++i) { - const ch = children[i]; - if (ch != null) { - api.appendChild(elm, createElm(ch, insertedVnodeQueue)); - } - } - } - else if (primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) - i.create(emptyNode, vnode); - if (i.insert) - insertedVnodeQueue.push(vnode); - } - } - else { - vnode.elm = api.createTextNode(vnode.text); - } - return vnode.elm; - } - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - if (ch != null) { - api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); - } - } - } - function invokeDestroyHook(vnode) { - let i, iLen, j, jLen, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.destroy))) - i(vnode); - for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) - cbs.destroy[i](vnode); - if (vnode.children !== undefined) { - for (j = 0, jLen = vnode.children.length; j < jLen; ++j) { - i = vnode.children[j]; - if (i != null && typeof i !== "string") { - invokeDestroyHook(i); - } - } - } - } - } - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - let i, iLen, listeners, rm, ch = vnodes[startIdx]; - if (ch != null) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) - cbs.remove[i](ch, rm); - if (isDef((i = ch.data)) && - isDef((i = i.hook)) && - isDef((i = i.remove))) { - i(ch, rm); - } - else { - rm(); - } - } - else { - // Text node - api.removeChild(parentElm, ch.elm); - } - } - } - } - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - let oldStartIdx = 0, newStartIdx = 0; - let oldEndIdx = oldCh.length - 1; - let oldStartVnode = oldCh[0]; - let oldEndVnode = oldCh[oldEndIdx]; - let newEndIdx = newCh.length - 1; - let newStartVnode = newCh[0]; - let newEndVnode = newCh[newEndIdx]; - let oldKeyToIdx; - let idxInOld; - let elmToMove; - let before; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left - } - else if (oldEndVnode == null) { - oldEndVnode = oldCh[--oldEndIdx]; - } - else if (newStartVnode == null) { - newStartVnode = newCh[++newStartIdx]; - } - else if (newEndVnode == null) { - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newEndVnode)) { - // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldEndVnode, newStartVnode)) { - // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { - // New element - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - else { - elmToMove = oldCh[idxInOld]; - if (elmToMove.sel !== newStartVnode.sel) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } - else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - } - newStartVnode = newCh[++newStartIdx]; - } - } - } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } - else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - } - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - let i, iLen, hook; - if (isDef((i = vnode.data)) && - isDef((hook = i.hook)) && - isDef((i = hook.prepatch))) { - i(oldVnode, vnode); - } - const elm = (vnode.elm = oldVnode.elm); - let oldCh = oldVnode.children; - let ch = vnode.children; - if (oldVnode === vnode) - return; - if (vnode.data !== undefined) { - for (i = 0, iLen = cbs.update.length; i < iLen; ++i) - cbs.update[i](oldVnode, vnode); - i = vnode.data.hook; - if (isDef(i) && isDef((i = i.update))) - i(oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) - updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } - else if (isDef(ch)) { - if (isDef(oldVnode.text)) - api.setTextContent(elm, ""); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } - else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ""); - } - } - else if (oldVnode.text !== vnode.text) { - if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - api.setTextContent(elm, vnode.text); - } - if (isDef(hook) && isDef((i = hook.postpatch))) { - i(oldVnode, vnode); - } - } - return function patch(oldVnode, vnode) { - let i, iLen, elm, parent; - const insertedVnodeQueue = []; - for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) - cbs.pre[i](); - if (!isVnode(oldVnode)) { - oldVnode = emptyNodeAt(oldVnode); - } - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } - else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - createElm(vnode, insertedVnodeQueue); - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0, iLen = cbs.post.length; i < iLen; ++i) - cbs.post[i](); - return vnode; - }; - } - //------------------------------------------------------------------------------ - // is.ts - //------------------------------------------------------------------------------ - const array = Array.isArray; - function primitive(s) { - return typeof s === "string" || typeof s === "number"; - } - function createElement(tagName) { - return document.createElement(tagName); - } - function createElementNS(namespaceURI, qualifiedName) { - return document.createElementNS(namespaceURI, qualifiedName); - } - function createTextNode(text) { - return document.createTextNode(text); - } - function createComment(text) { - return document.createComment(text); - } - function insertBefore(parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); - } - function removeChild(node, child) { - node.removeChild(child); - } - function appendChild(node, child) { - node.appendChild(child); - } - function parentNode(node) { - return node.parentNode; - } - function nextSibling(node) { - return node.nextSibling; - } - function tagName(elm) { - return elm.tagName; - } - function setTextContent(node, text) { - node.textContent = text; - } - function getTextContent(node) { - return node.textContent; - } - function isElement(node) { - return node.nodeType === 1; - } - function isText(node) { - return node.nodeType === 3; - } - function isComment(node) { - return node.nodeType === 8; - } - const htmlDomApi = { - createElement, - createElementNS, - createTextNode, - createComment, - insertBefore, - removeChild, - appendChild, - parentNode, - nextSibling, - tagName, - setTextContent, - getTextContent, - isElement, - isText, - isComment - }; - function addNS(data, children, sel) { - data.ns = "http://www.w3.org/2000/svg"; - if (sel !== "foreignObject" && children !== undefined) { - for (let i = 0, iLen = children.length; i < iLen; ++i) { - let childData = children[i].data; - if (childData !== undefined) { - addNS(childData, children[i].children, children[i].sel); - } - } - } - } - function h(sel, b, c) { - var data = {}, children, text, i, iLen; - if (c !== undefined) { - data = b; - if (array(c)) { - children = c; - } - else if (primitive(c)) { - text = c; - } - else if (c && c.sel) { - children = [c]; - } - } - else if (b !== undefined) { - if (array(b)) { - children = b; - } - else if (primitive(b)) { - text = b; - } - else if (b && b.sel) { - children = [b]; - } - else { - data = b; - } - } - if (children !== undefined) { - for (i = 0, iLen = children.length; i < iLen; ++i) { - if (primitive(children[i])) - children[i] = vnode(undefined, undefined, undefined, children[i], undefined); - } - } - if (sel[0] === "s" && - sel[1] === "v" && - sel[2] === "g" && - (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { - addNS(data, children, sel); - } - return vnode(sel, data, children, text, undefined); - } - function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; - if (!oldProps && !props) - return; - if (oldProps === props) - return; - oldProps = oldProps || {}; - props = props || {}; - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - const propsModule = { - create: updateProps, - update: updateProps - }; - function invokeHandler(handler, vnode, event) { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } - else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); - } - else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); - } - } - else { - // call multiple handlers - for (let i = 0, iLen = handler.length; i < iLen; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } - } - function handleEvent(event, vnode) { - var name = event.type, on = vnode.data.on; - // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } - } - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - oldElm.removeEventListener(name, oldListener, false); - } - } - else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = (vnode.listener = - oldVnode.listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - elm.addEventListener(name, listener, false); - } - } - else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } - } - const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners - }; - const xlinkNS = "http://www.w3.org/1999/xlink"; - const xmlNS = "http://www.w3.org/XML/1998/namespace"; - const colonChar = 58; - const xChar = 120; - function updateAttrs(oldVnode, vnode) { - var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) - return; - if (oldAttrs === attrs) - return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } - else if (cur === false) { - elm.removeAttribute(key); - } - else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } - else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } - else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } - else { - elm.setAttribute(key, cur); - } - } - } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } - } - const attrsModule = { - create: updateAttrs, - update: updateAttrs - }; - const patch = init([eventListenersModule, attrsModule, propsModule]); - - /** - * Owl QWeb Expression Parser - * - * Owl needs in various contexts to be able to understand the structure of a - * string representing a javascript expression. The usual goal is to be able - * to rewrite some variables. For example, if a template has - * - * ```xml - * ... - * ``` - * - * this needs to be translated in something like this: - * - * ```js - * if (context["computeSomething"]({val: context["state"].val})) { ... } - * ``` - * - * This file contains the implementation of an extremely naive tokenizer/parser - * and evaluator for javascript expressions. The supported grammar is basically - * only expressive enough to understand the shape of objects, of arrays, and - * various operators. - */ - //------------------------------------------------------------------------------ - // Misc types, constants and helpers - //------------------------------------------------------------------------------ - const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(","); - const WORD_REPLACEMENT = { - and: "&&", - or: "||", - gt: ">", - gte: ">=", - lt: "<", - lte: "<=" - }; - const STATIC_TOKEN_MAP = { - "{": "LEFT_BRACE", - "}": "RIGHT_BRACE", - "[": "LEFT_BRACKET", - "]": "RIGHT_BRACKET", - ":": "COLON", - ",": "COMMA", - "(": "LEFT_PAREN", - ")": "RIGHT_PAREN" - }; - const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(','); - let tokenizeString = function (expr) { - let s = expr[0]; - let start = s; - if (s !== "'" && s !== '"') { - return false; - } - let i = 1; - let cur; - while (expr[i] && expr[i] !== start) { - cur = expr[i]; - s += cur; - if (cur === "\\") { - i++; - cur = expr[i]; - if (!cur) { - throw new Error("Invalid expression"); - } - s += cur; - } - i++; - } - if (expr[i] !== start) { - throw new Error("Invalid expression"); - } - s += start; - return { type: "VALUE", value: s }; - }; - let tokenizeNumber = function (expr) { - let s = expr[0]; - if (s && s.match(/[0-9]/)) { - let i = 1; - while (expr[i] && expr[i].match(/[0-9]|\./)) { - s += expr[i]; - i++; - } - return { type: "VALUE", value: s }; - } - else { - return false; - } - }; - let tokenizeSymbol = function (expr) { - let s = expr[0]; - if (s && s.match(/[a-zA-Z_\$]/)) { - let i = 1; - while (expr[i] && expr[i].match(/\w/)) { - s += expr[i]; - i++; - } - if (s in WORD_REPLACEMENT) { - return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length }; - } - return { type: "SYMBOL", value: s }; - } - else { - return false; - } - }; - const tokenizeStatic = function (expr) { - const char = expr[0]; - if (char && char in STATIC_TOKEN_MAP) { - return { type: STATIC_TOKEN_MAP[char], value: char }; - } - return false; - }; - const tokenizeOperator = function (expr) { - for (let op of OPERATORS) { - if (expr.startsWith(op)) { - return { type: "OPERATOR", value: op }; - } - } - return false; - }; - const TOKENIZERS = [ - tokenizeString, - tokenizeNumber, - tokenizeSymbol, - tokenizeStatic, - tokenizeOperator - ]; - /** - * Convert a javascript expression (as a string) into a list of tokens. For - * example: `tokenize("1 + b")` will return: - * ```js - * [ - * {type: "VALUE", value: "1"}, - * {type: "OPERATOR", value: "+"}, - * {type: "SYMBOL", value: "b"} - * ] - * ``` - */ - function tokenize(expr) { - const result = []; - let token = true; - while (token) { - expr = expr.trim(); - if (expr) { - for (let tokenizer of TOKENIZERS) { - token = tokenizer(expr); - if (token) { - result.push(token); - expr = expr.slice(token.size || token.value.length); - break; - } - } - } - else { - token = false; - } - } - if (expr.length) { - throw new Error(`Tokenizer error: could not tokenize "${expr}"`); - } - return result; - } - //------------------------------------------------------------------------------ - // Expression "evaluator" - //------------------------------------------------------------------------------ - /** - * 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 - * proper lookups in the context. - * - * Usually, this kind of code would be very simple to do if we had an AST (so, - * if we had a javascript parser), since then, we would only need to find the - * variables and replace them. However, a parser is more complicated, and there - * are no standard builtin parser API. - * - * Since this method is applied to simple javasript expressions, and the work to - * be done is actually quite simple, we actually can get away with not using a - * parser, which helps with the code size. - * - * Here is the heuristic used by this method to determine if a token is a - * variable: - * - by default, all symbols are considered a variable - * - unless the previous token is a dot (in that case, this is a property: `a.b`) - * - or if the previous token is a left brace or a comma, and the next token is - * a colon (in that case, this is an object key: `{a: b}`) - */ - function compileExpr(expr, vars) { - const tokens = tokenize(expr); - let result = ""; - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { - // we need to find if it is a variable - let isVar = true; - let prevToken = tokens[i - 1]; - if (prevToken) { - if (prevToken.type === "OPERATOR" && prevToken.value === ".") { - isVar = false; - } - else if (prevToken.type === "LEFT_BRACE" || - prevToken.type === "COMMA") { - let nextToken = tokens[i + 1]; - if (nextToken && nextToken.type === "COLON") { - isVar = false; - } - } - } - if (isVar) { - if (token.value in vars && "id" in vars[token.value]) { - token.value = vars[token.value].id; - } - else { - token.value = `context['${token.value}']`; - } - } - } - result += token.value; - } - return result; - } - - //------------------------------------------------------------------------------ - // Const/global stuff/helpers - //------------------------------------------------------------------------------ - const DISABLED_TAGS = [ - "input", - "textarea", - "button", - "select", - "option", - "optgroup" - ]; - const lineBreakRE = /[\r\n]/; - const whitespaceRE = /\s+/g; - const DIRECTIVE_NAMES = { - name: 1, - att: 1, - attf: 1, - key: 1 - }; - const DIRECTIVES = []; - const NODE_HOOKS_PARAMS = { - create: "(_, n)", - insert: "vn", - remove: "(vn, rm)" - }; - const UTILS = { - h: h, - objectToAttrString(obj) { - let classes = []; - for (let k in obj) { - if (obj[k]) { - classes.push(k); - } - } - return classes.join(" "); - }, - shallowEqual(p1, p2) { - for (let k in p1) { - if (p1[k] !== p2[k]) { - return false; - } - } - return true; - } - }; - function parseXML(xml) { - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, "text/xml"); - if (doc.getElementsByTagName("parsererror").length) { - throw new Error("Invalid XML in template"); - } - return doc; - } - let nextID = 1; - //------------------------------------------------------------------------------ - // QWeb rendering engine - //------------------------------------------------------------------------------ - class QWeb { - constructor(data) { - this.templates = {}; - this.utils = UTILS; - // the id field is useful to be able to hash qweb instances. The current - // use case is that component's templates are qweb dependant, and need to be - // able to map a qweb instance to a template name. - this.id = nextID++; - // slots contains sub templates defined with t-set inside t-widget nodes, and - // are meant to be used by the t-slot directive. - this.slots = {}; - this.nextSlotId = 1; - if (data) { - this.addTemplates(data); - } - this.addTemplate("default", "
"); - } - static addDirective(directive) { - DIRECTIVES.push(directive); - DIRECTIVE_NAMES[directive.name] = 1; - DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); - if (directive.extraNames) { - directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1)); - } - } - static register(name, Component) { - if (QWeb.widgets[name]) { - throw new Error(`Component '${name}' has already been registered`); - } - QWeb.widgets[name] = Component; - } - /** - * Add a template to the internal template map. Note that it is not - * immediately compiled. - */ - addTemplate(name, xmlString) { - const doc = parseXML(xmlString); - if (!doc.firstChild) { - throw new Error("Invalid template (should not be empty)"); - } - this._addTemplate(name, doc.firstChild); - } - /** - * Load templates from a xml (as a string). This will look up for the first - * tag, and will consider each child of this as a template, with - * the name given by the t-name attribute. - */ - addTemplates(xmlstr) { - const doc = parseXML(xmlstr); - const templates = doc.getElementsByTagName("templates")[0]; - if (!templates) { - return; - } - for (let elem of templates.children) { - const name = elem.getAttribute("t-name"); - this._addTemplate(name, elem); - } - } - _addTemplate(name, elem) { - if (name in this.templates) { - throw new Error(`Template ${name} already defined`); - } - this._processTemplate(elem); - const template = { - elem, - fn: (context, extra) => { - const compiledFunction = this._compile(name, elem); - template.fn = compiledFunction; - return compiledFunction.call(this, context, extra); - } - }; - this.templates[name] = template; - } - _processTemplate(elem) { - let tbranch = elem.querySelectorAll("[t-elif], [t-else]"); - for (let i = 0, ilen = tbranch.length; i < ilen; i++) { - let node = tbranch[i]; - let prevElem = node.previousElementSibling; - let pattr = function (name) { - return prevElem.getAttribute(name); - }; - let nattr = function (name) { - return +!!node.getAttribute(name); - }; - if (prevElem && (pattr("t-if") || pattr("t-elif"))) { - if (pattr("t-foreach")) { - throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else"); - } - if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) { - return a + b; - }) > 1) { - throw new Error("Only one conditional branching directive is allowed per node"); - } - // All text nodes between branch nodes are removed - let textNode; - while ((textNode = node.previousSibling) !== prevElem) { - if (textNode.nodeValue.trim().length) { - throw new Error("text is not allowed between branching directives"); - } - textNode.remove(); - } - } - else { - throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive"); - } - } - } - /** - * Render a template - * - * @param {string} name the template should already have been added - */ - render(name, context = {}, extra = null) { - const template = this.templates[name]; - if (!template) { - throw new Error(`Template ${name} does not exist`); - } - return template.fn.call(this, context, extra); - } - _compile(name, elem) { - const isDebug = elem.attributes.hasOwnProperty("t-debug"); - const ctx = new Context(name); - this._compileNode(elem, ctx); - if (ctx.shouldProtectContext) { - ctx.code.unshift(" context = Object.create(context);"); - } - if (ctx.shouldDefineOwner) { - // this is necessary to prevent some directives (t-forach for ex) to - // pollute the rendering context by adding some keys in it. - ctx.code.unshift(" let owner = context;"); - } - if (ctx.shouldDefineQWeb) { - ctx.code.unshift(" let QWeb = this.constructor;"); - } - if (ctx.shouldDefineUtils) { - ctx.code.unshift(" let utils = this.utils;"); - } - if (!ctx.rootNode) { - throw new Error("A template should have one root node"); - } - ctx.addLine(`return vn${ctx.rootNode};`); - let template; - try { - template = new Function("context", "extra", ctx.code.join("\n")); - } - catch (e) { - const templateName = ctx.templateName.replace(/`/g, "'"); - console.groupCollapsed(`Invalid Code generated by ${templateName}`); - console.warn(ctx.code.join("\n")); - console.groupEnd(); - throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`); - } - if (isDebug) { - console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` + - template.toString()); - } - return template; - } - /** - * Generate code from an xml node - * - */ - _compileNode(node, ctx) { - if (!(node instanceof Element)) { - // this is a text node, there are no directive to apply - let text = node.textContent; - if (!ctx.inPreTag) { - if (lineBreakRE.test(text) && !text.trim()) { - return; - } - text = text.replace(whitespaceRE, " "); - } - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); - } - else if (ctx.parentTextNode) { - ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`); - } - else { - // this is an unusual situation: this text node is the result of the - // template rendering. - let nodeID = ctx.generateID(); - ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); - ctx.rootContext.rootNode = nodeID; - ctx.rootContext.parentTextNode = nodeID; - } - return; - } - const attributes = node.attributes; - const validDirectives = []; - let withHandlers = false; - // maybe this is not optimal: we iterate on all attributes here, and again - // just after for each directive. - for (let i = 0; i < attributes.length; i++) { - let attrName = attributes[i].name; - if (attrName.startsWith("t-")) { - let dName = attrName.slice(2).split("-")[0]; - if (!(dName in DIRECTIVE_NAMES)) { - throw new Error(`Unknown QWeb directive: '${attrName}'`); - } - } - } - const DIR_N = DIRECTIVES.length; - const ATTR_N = attributes.length; - for (let i = 0; i < DIR_N; i++) { - let directive = DIRECTIVES[i]; - let fullName; - let value; - for (let j = 0; j < ATTR_N; j++) { - const name = attributes[j].name; - if (name === "t-" + directive.name || - name.startsWith("t-" + directive.name + "-")) { - fullName = name; - value = attributes[j].textContent; - validDirectives.push({ directive, value, fullName }); - if (directive.name === "on") { - withHandlers = true; - } - } - } - } - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeEncounter) { - const isDone = directive.atNodeEncounter({ - node, - qweb: this, - ctx, - fullName, - value - }); - if (isDone) { - return; - } - } - } - if (node.nodeName !== "t") { - 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); - }; - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeCreation) { - directive.atNodeCreation({ - node, - qweb: this, - ctx, - fullName, - value, - nodeID, - addNodeHook - }); - } - } - if (Object.keys(nodeHooks).length) { - ctx.addLine(`p${nodeID}.hook = {`); - for (let hook in nodeHooks) { - ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`); - for (let handler of nodeHooks[hook]) { - ctx.addLine(` ${handler}`); - } - ctx.addLine(` },`); - } - ctx.addLine(`};`); - } - } - if (node.nodeName === "pre") { - ctx = ctx.subContext("inPreTag", true); - } - this._compileChildren(node, ctx); - for (let { directive, value, fullName } of validDirectives) { - if (directive.finalize) { - directive.finalize({ node, qweb: this, ctx, fullName, value }); - } - } - } - _compileGenericNode(node, ctx, withHandlers = true) { - // nodeType 1 is generic tag - if (node.nodeType !== 1) { - throw new Error("unsupported node type"); - } - const attributes = node.attributes; - const attrs = []; - const props = []; - const tattrs = []; - function handleBooleanProps(key, val) { - let isProp = false; - if (node.nodeName === "input" && key === "checked") { - let type = node.getAttribute("type"); - if (type === "checkbox" || type === "radio") { - isProp = true; - } - } - if (node.nodeName === "option" && key === "selected") { - isProp = true; - } - if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { - isProp = true; - } - if ((key === "readonly" && node.nodeName === "input") || - node.nodeName === "textarea") { - isProp = true; - } - if (isProp) { - props.push(`${key}: _${val}`); - } - } - for (let i = 0; i < attributes.length; i++) { - let name = attributes[i].name; - const value = attributes[i].textContent; - // regular attributes - if (!name.startsWith("t-") && - !node.getAttribute("t-attf-" + name)) { - const attID = ctx.generateID(); - ctx.addLine(`var _${attID} = '${value}';`); - if (!name.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - name = '"' + name + '"'; - } - attrs.push(`${name}: _${attID}`); - handleBooleanProps(name, attID); - } - // dynamic attributes - if (name.startsWith("t-att-")) { - let attName = name.slice(6); - const v = ctx.getValue(value); - let formattedValue = v.id || ctx.formatExpression(v); - if (formattedValue[0] === "{" && - formattedValue[formattedValue.length - 1] === "}") { - formattedValue = `this.utils.objectToAttrString(${formattedValue})`; - } - const attID = ctx.generateID(); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - // we need to combine dynamic with non dynamic attributes: - // class="a" t-att-class="'yop'" should be rendered as class="a yop" - const attValue = node.getAttribute(attName); - if (attValue) { - const attValueID = ctx.generateID(); - ctx.addLine(`var _${attValueID} = ${formattedValue};`); - formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; - const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); - attrs.splice(attrIndex, 1); - } - ctx.addLine(`var _${attID} = ${formattedValue};`); - attrs.push(`${attName}: _${attID}`); - handleBooleanProps(attName, attID); - } - if (name.startsWith("t-attf-")) { - let attName = name.slice(7); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - const formattedExpr = ctx.interpolate(value); - const attID = ctx.generateID(); - let staticVal = node.getAttribute(attName); - if (staticVal) { - ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`); - } - else { - ctx.addLine(`var _${attID} = ${formattedExpr};`); - } - attrs.push(`${attName}: _${attID}`); - } - // t-att= attributes - if (name === "t-att") { - let id = ctx.generateID(); - ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`); - tattrs.push(id); - } - } - let nodeID = ctx.generateID(); - let nodeKey = node.getAttribute("t-key"); - if (nodeKey) { - nodeKey = ctx.formatExpression(nodeKey); - } - else { - nodeKey = nodeID; - } - const parts = [`key:${nodeKey}`]; - if (attrs.length + tattrs.length > 0) { - parts.push(`attrs:{${attrs.join(",")}}`); - } - if (props.length > 0) { - parts.push(`props:{${props.join(",")}}`); - } - if (withHandlers) { - parts.push(`on:{}`); - } - ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`); - for (let id of tattrs) { - ctx.addIf(`_${id} instanceof Array`); - ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`); - ctx.addElse(); - ctx.addLine(`for (let key in _${id}) {`); - ctx.indent(); - ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`); - ctx.dedent(); - ctx.addLine(`}`); - ctx.closeIf(); - } - ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); - } - return nodeID; - } - _compileChildren(node, ctx) { - if (node.childNodes.length > 0) { - for (let child of Array.from(node.childNodes)) { - this._compileNode(child, ctx); - } - } - } - } - QWeb.widgets = Object.create(null); - // dev mode enables better error messages or more costly validations - QWeb.dev = false; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class Context { - constructor(name) { - this.nextID = 1; - this.code = []; - this.variables = {}; - this.escaping = false; - this.parentNode = null; - this.parentTextNode = null; - this.rootNode = null; - this.indentLevel = 0; - this.shouldDefineOwner = false; - this.shouldDefineQWeb = false; - this.shouldDefineUtils = false; - this.shouldProtectContext = false; - this.inLoop = false; - this.inPreTag = false; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("var h = this.utils.h;"); - } - generateID() { - const id = this.rootContext.nextID++; - return id; - } - withParent(node) { - if (this === this.rootContext && (this.parentNode || this.parentTextNode)) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - return this.subContext("parentNode", node); - } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.indentLevel++; - } - dedent() { - this.indentLevel--; - } - addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - closeIf() { - this.dedent(); - this.addLine("}"); - } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - /** - * Prepare an expression for being consumed at render time. Its main job - * is to - * - replace unknown variables by a lookup in the context - * - replace already defined variables by their internal name - */ - formatExpression(expr) { - return compileExpr(expr, this.variables); - } - /** - * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted and enclosed in - * parentheses). - * For instance: - * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') - */ - interpolate(s) { - let matches = s.match(/\{\{.*?\}\}/g); - if (matches && matches[0].length === s.length) { - return `(${this.formatExpression(s.slice(2, -2))})`; - } - let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); - return "`" + r + "`"; - } - } - - // If a component does not define explicitely a template - // key, it needs to find a template with its name (or a parent's). This is - // qweb dependant, so we need a place to store this information indexed by - // qweb instances. - const TEMPLATE_MAP = {}; - //------------------------------------------------------------------------------ - // Widget - //------------------------------------------------------------------------------ - let nextId = 1; - class Component { - //-------------------------------------------------------------------------- - // Lifecycle - //-------------------------------------------------------------------------- - /** - * Creates an instance of Component. - * - * The root widget of a component tree needs an environment: - * - * ```javascript - * const root = new RootWidget(env, props); - * ``` - * - * Every other widget simply needs a reference to its parent: - * - * ```javascript - * const child = new SomeWidget(parent, props); - * ``` - * - * Note that most of the time, only the root widget needs to be created by - * hand. Other widgets should be created automatically by the framework (with - * the t-widget directive in a template) - */ - constructor(parent, props) { - this.refs = {}; - const defaultProps = this.constructor.defaultProps; - if (defaultProps) { - props = this._applyDefaultProps(props, defaultProps); - } - if (QWeb.dev) { - this._validateProps(props || {}); - } - // is this a good idea? - // Pro: if props is empty, we can create easily a widget - // Con: this is not really safe - // Pro: but creating widget (by a template) is always unsafe anyway - this.props = props || {}; - let id = nextId++; - let p = null; - if (parent instanceof Component) { - p = parent; - this.env = parent.env; - parent.__owl__.children[id] = this; - } - else { - this.env = parent; - } - this.__owl__ = { - id: id, - vnode: null, - isMounted: false, - isDestroyed: false, - parent: p, - children: {}, - cmap: {}, - renderId: 1, - renderPromise: null, - renderProps: props || null, - boundHandlers: {}, - mountedHandlers: {} - }; - } - /** - * The `el` is the root element of the widget. Note that it could be null: - * this is the case if the widget is not mounted yet, or is destroyed. - */ - get el() { - return this.__owl__.vnode ? this.__owl__.vnode.elm : null; - } - /** - * willStart is an asynchronous hook that can be implemented to perform some - * action before the initial rendering of a component. - * - * It will be called exactly once before the initial rendering. It is useful - * in some cases, for example, to load external assets (such as a JS library) - * before the widget is rendered. - * - * Note that a slow willStart method will slow down the rendering of the user - * interface. Therefore, some effort should be made to make this method as - * fast as possible. - * - * Note: this method should not be called manually. - */ - async willStart() { } - /** - * mounted is a hook that is called each time a component is attached to the - * DOM. This is a good place to add some listeners, or to interact with the - * DOM, if the component needs to perform some measure for example. - * - * Note: this method should not be called manually. - * - * @see willUnmount - */ - mounted() { } - /** - * The willUpdateProps is an asynchronous hook, called just before new props - * are set. This is useful if the component needs some asynchronous task - * performed, depending on the props (for example, assuming that the props are - * some record Id, fetching the record data). - * - * This hook is not called during the first render (but willStart is called - * and performs a similar job). - */ - async willUpdateProps(nextProps) { } - /** - * The willPatch hook is called just before the DOM patching process starts. - * It is not called on the initial render. This is useful to get some - * information which are in the DOM. For example, the current position of the - * scrollbar - * - * The return value of willPatch will be given to the patched function. - */ - willPatch() { } - /** - * This hook is called whenever a component did actually update its props, - * state or env. - * - * This method is not called on the initial render. It is useful to interact - * with the DOM (for example, through an external library) whenever the - * component was updated. - * - * Updating the widget state in this hook is possible, but not encouraged. - * One need to be careful, because updates here will cause rerender, which in - * turn will cause other calls to updated. So, we need to be particularly - * careful at avoiding endless cycles. - * - * The snapshot parameter is the result of the call to willPatch. - */ - patched(snapshot) { } - /** - * willUnmount is a hook that is called each time just before a component is - * unmounted from the DOM. This is a good place to remove some listeners, for - * example. - * - * Note: this method should not be called manually. - * - * @see mounted - */ - willUnmount() { } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - /** - * Mount the component to a target element. - * - * This should only be done if the component was created manually. Components - * created declaratively in templates are managed by the Owl system. - */ - async mount(target) { - const vnode = await this._prepare(); - if (this.__owl__.isDestroyed) { - // component was destroyed before we get here... - return; - } - this._patch(vnode); - target.appendChild(this.el); - if (document.body.contains(target)) { - this._callMounted(); - } - } - unmount() { - if (this.__owl__.isMounted) { - this._callWillUnmount(); - this.el.remove(); - } - } - async render(force = false, patchQueue) { - const __owl__ = this.__owl__; - if (!__owl__.isMounted) { - return; - } - const shouldPatch = !patchQueue; - if (shouldPatch) { - patchQueue = []; - } - const renderVDom = this._render(force, patchQueue); - const renderId = __owl__.renderId; - await renderVDom; - if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) { - // we only update the vnode and the actual DOM if no other rendering - // occurred between now and when the render method was initially called. - const patchLen = patchQueue.length; - for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch.push(patch[0].willPatch()); - } - for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch[0]._patch(patch[1]); - } - for (let i = patchLen - 1; i >= 0; i--) { - const patch = patchQueue[i]; - patch[0].patched(patch[2]); - } - } - } - /** - * Destroy the component. This operation is quite complex: - * - it recursively destroy all children - * - call the willUnmount hooks if necessary - * - remove the dom node from the dom - * - * This should only be called manually if you created the widget. Most widgets - * will be automatically destroyed. - */ - destroy() { - const __owl__ = this.__owl__; - if (!__owl__.isDestroyed) { - const el = this.el; - this._destroy(__owl__.parent); - if (el) { - el.remove(); - } - } - } - /** - * This method is called by the component system whenever its props are - * updated. If it returns true, then the component will be rendered. - * Otherwise, it will skip the rendering (also, its props will not be updated) - */ - shouldUpdate(nextProps) { - return true; - } - /** - * This method is the correct way to update the environment of a widget. Doing - * this will cause a full rerender of the widget and its children, so this is - * an operation that should not be done frequently. - * - * A good usecase for updating the environment would be to update some mostly - * static config keys, such as a boolean to determine if we are in mobile - * mode or not. - */ - async updateEnv(nextEnv) { - const __owl__ = this.__owl__; - if (__owl__.parent && __owl__.parent.env === this.env) { - this.env = Object.create(this.env); - } - Object.assign(this.env, nextEnv); - if (__owl__.isMounted) { - await this.render(true); - } - } - /** - * Emit a custom event of type 'eventType' with the given 'payload' on the - * component's el, if it exists. However, note that the event will only bubble - * up to the parent DOM nodes. Thus, it must be called between mounted() and - * willUnmount(). - */ - trigger(eventType, payload) { - if (this.el) { - const ev = new CustomEvent(eventType, { - bubbles: true, - cancelable: true, - detail: payload - }); - this.el.dispatchEvent(ev); - } - } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - _destroy(parent) { - const __owl__ = this.__owl__; - const isMounted = __owl__.isMounted; - if (isMounted) { - this.willUnmount(); - __owl__.isMounted = false; - } - const children = __owl__.children; - for (let key in children) { - children[key]._destroy(this); - } - if (parent) { - let id = __owl__.id; - delete parent.__owl__.children[id]; - __owl__.parent = null; - } - __owl__.isDestroyed = true; - delete __owl__.vnode; - } - _callMounted() { - const __owl__ = this.__owl__; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (!comp.__owl__.isMounted && this.el.contains(comp.el)) { - comp._callMounted(); - } - } - __owl__.isMounted = true; - const handlers = __owl__.mountedHandlers; - for (let key in handlers) { - handlers[key](); - } - this.mounted(); - } - _callWillUnmount() { - this.willUnmount(); - const __owl__ = this.__owl__; - __owl__.isMounted = false; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (comp.__owl__.isMounted) { - comp._callWillUnmount(); - } - } - } - async _updateProps(nextProps, forceUpdate = false, patchQueue) { - const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); - if (shouldUpdate) { - const defaultProps = this.constructor.defaultProps; - if (defaultProps) { - nextProps = this._applyDefaultProps(nextProps, defaultProps); - } - if (QWeb.dev) { - this._validateProps(nextProps); - } - await this.willUpdateProps(nextProps); - this.props = nextProps; - await this.render(forceUpdate, patchQueue); - } - } - _patch(vnode) { - const __owl__ = this.__owl__; - __owl__.renderPromise = null; - const target = __owl__.vnode || document.createElement(vnode.sel); - __owl__.vnode = patch(target, vnode); - } - _prepare() { - const __owl__ = this.__owl__; - __owl__.renderProps = this.props; - __owl__.renderPromise = this._prepareAndRender(); - return __owl__.renderPromise; - } - async _prepareAndRender() { - await this.willStart(); - const __owl__ = this.__owl__; - if (__owl__.isDestroyed) { - return Promise.resolve(h("div")); - } - const qweb = this.env.qweb; - if (!this.template) { - let tmap = TEMPLATE_MAP[qweb.id]; - if (!tmap) { - tmap = {}; - TEMPLATE_MAP[qweb.id] = tmap; - } - let p = this.constructor; - let name = p.name; - let template = tmap[name]; - if (template) { - this.template = template; - } - else { - while ((template = p.name) && - !(template in qweb.templates) && - p !== Component) { - p = p.__proto__; - } - if (p === Component) { - this.template = "default"; - } - else { - tmap[name] = template; - this.template = template; - } - } - } - __owl__.render = qweb.render.bind(qweb, this.template); - this._observeState(); - return this._render(); - } - async _render(force = false, patchQueue = []) { - const __owl__ = this.__owl__; - __owl__.renderId++; - const promises = []; - const patch = [this]; - if (__owl__.isMounted) { - patchQueue.push(patch); - } - if (__owl__.observer) { - __owl__.observer.allowMutations = false; - } - let vnode = __owl__.render(this, { - promises, - handlers: __owl__.boundHandlers, - mountedHandlers: __owl__.mountedHandlers, - forceUpdate: force, - patchQueue - }); - patch.push(vnode); - if (__owl__.observer) { - __owl__.observer.allowMutations = true; - } - // this part is critical for the patching process to be done correctly. The - // tricky part is that a child widget can be rerendered on its own, which - // will update its own vnode representation without the knowledge of the - // parent widget. With this, we make sure that the parent widget will be - // able to patch itself properly after - vnode.key = __owl__.id; - __owl__.renderProps = this.props; - __owl__.renderPromise = Promise.all(promises).then(() => vnode); - return __owl__.renderPromise; - } - /** - * Only called by qweb t-widget directive - */ - _mount(vnode, elm) { - const __owl__ = this.__owl__; - __owl__.vnode = patch(elm, vnode); - if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) { - this._callMounted(); - } - return __owl__.vnode; - } - /** - * Only called by qweb t-widget directive (when t-keepalive is set) - */ - _remount() { - const __owl__ = this.__owl__; - if (!__owl__.isMounted) { - __owl__.isMounted = true; - this.mounted(); - } - } - _observeState() { - if (this.state) { - const __owl__ = this.__owl__; - __owl__.observer = new Observer(); - __owl__.observer.observe(this.state); - __owl__.observer.notifyCB = this.render.bind(this); - } - } - /** - * Apply default props (only top level). - * - * Note that this method does not modify in place the props, it returns a new - * prop object - */ - _applyDefaultProps(props, defaultProps) { - props = props ? Object.create(props) : {}; - for (let propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - return props; - } - /** - * Validate the component props (or next props) against the (static) props - * description. This is potentially an expensive operation: it may needs to - * visit recursively the props and all the children to check if they are valid. - * This is why it is only done in 'dev' mode. - */ - _validateProps(props) { - const propsDef = this.constructor.props; - if (propsDef instanceof Array) { - // list of strings (prop names) - for (let i = 0, l = propsDef.length; i < l; i++) { - if (!(propsDef[i] in props)) { - throw new Error(`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`); - } - } - } - else if (propsDef) { - // propsDef is an object now - for (let propName in propsDef) { - if (!(propName in props)) { - if (propsDef[propName] && !propsDef[propName].optional) { - throw new Error(`Missing props '${propName}' (widget '${this.constructor.name}')`); - } - else { - break; - } - } - let isValid = isValidProp(props[propName], propsDef[propName]); - if (!isValid) { - throw new Error(`Props '${propName}' of invalid type in widget '${this.constructor.name}'`); - } - } - } - } - } - //------------------------------------------------------------------------------ - // Prop validation helper - //------------------------------------------------------------------------------ - /** - * Check if an invidual prop value matches its (static) prop definition - */ - function isValidProp(prop, propDef) { - if (typeof propDef === "function") { - // Check if a value is constructed by some Constructor. Note that there is a - // slight abuse of language: we want to consider primitive values as well. - // - // So, even though 1 is not an instance of Number, we want to consider that - // it is valid. - if (typeof prop === "object") { - return prop instanceof propDef; - } - return typeof prop === propDef.name.toLowerCase(); - } - else if (propDef instanceof Array) { - // If this code is executed, this means that we want to check if a prop - // matches at least one of its descriptor. - let result = false; - for (let i = 0, iLen = propDef.length; i < iLen; i++) { - result = result || isValidProp(prop, propDef[i]); - } - return result; - } - // propsDef is an object - let result = isValidProp(prop, propDef.type); - if (propDef.type === Array) { - for (let i = 0, iLen = prop.length; i < iLen; i++) { - result = result && isValidProp(prop[i], propDef.element); - } - } - if (propDef.type === Object) { - const shape = propDef.shape; - for (let key in shape) { - result = result && isValidProp(prop[key], shape[key]); - } - } - return result; - } - - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a widget or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } - - /** - * Owl QWeb Directives - * - * This file contains the implementation of most standard QWeb directives: - * - t-esc - * - t-raw - * - t-set/t-value - * - t-if/t-elif/t-else - * - t-call - * - t-foreach/t-as - * - t-debug - * - t-log - */ - //------------------------------------------------------------------------------ - // t-esc and t-raw - //------------------------------------------------------------------------------ - UTILS.getFragment = function (str) { - const temp = document.createElement("template"); - temp.innerHTML = str; - return temp.content; - }; - function compileValueNode(value, node, qweb, ctx) { - if (value === "0" && ctx.caller) { - qweb._compileNode(ctx.caller, ctx); - return; - } - if (value.xml instanceof NodeList) { - for (let node of Array.from(value.xml)) { - qweb._compileNode(node, ctx); - } - return; - } - let exprID; - if (typeof value === "string") { - exprID = `_${ctx.generateID()}`; - ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`); - } - else { - exprID = value.id; - } - ctx.addIf(`${exprID} || ${exprID} === 0`); - if (ctx.escaping) { - if (ctx.parentTextNode) { - ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`); - } - else if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`); - } - else { - let nodeID = ctx.generateID(); - ctx.rootContext.rootNode = nodeID; - ctx.rootContext.parentTextNode = nodeID; - ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`); - } - } - else { - let fragID = ctx.generateID(); - ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); - let tempNodeID = ctx.generateID(); - ctx.addLine(`var p${tempNodeID} = {hook: {`); - ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); - ctx.addLine(`}};`); - ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); - ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); - } - if (node.childNodes.length) { - ctx.addElse(); - qweb._compileChildren(node, ctx); - } - ctx.closeIf(); - } - QWeb.addDirective({ - name: "esc", - priority: 70, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-esc")); - compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); - return true; - } - }); - QWeb.addDirective({ - name: "raw", - priority: 80, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-raw")); - compileValueNode(value, node, qweb, ctx); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-set - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "set", - extraNames: ["value"], - priority: 60, - atNodeEncounter({ node, ctx }) { - const variable = node.getAttribute("t-set"); - let value = node.getAttribute("t-value"); - if (value) { - const formattedValue = ctx.formatExpression(value); - if (ctx.variables.hasOwnProperty(variable)) { - ctx.addLine(`${ctx.variables[variable].id} = ${formattedValue}`); - } - else { - const varName = `_${ctx.generateID()}`; - ctx.addLine(`var ${varName} = ${formattedValue};`); - ctx.variables[variable] = { - id: varName, - expr: formattedValue - }; - } - } - else { - ctx.variables[variable] = { - xml: node.childNodes - }; - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-if, t-elif, t-else - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "if", - priority: 20, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-if")); - ctx.addIf(`${ctx.formatExpression(cond)}`); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - QWeb.addDirective({ - name: "elif", - priority: 30, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-elif")); - ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - QWeb.addDirective({ - name: "else", - priority: 40, - atNodeEncounter({ ctx }) { - ctx.addLine(`else {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }); - //------------------------------------------------------------------------------ - // t-call - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "call", - priority: 50, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - throw new Error("Invalid tag for t-call directive (should be 't')"); - } - const subTemplate = node.getAttribute("t-call"); - const nodeTemplate = qweb.templates[subTemplate]; - if (!nodeTemplate) { - throw new Error(`Cannot find template "${subTemplate}" (t-call)`); - } - const nodeCopy = node.cloneNode(true); - nodeCopy.removeAttribute("t-call"); - // extract variables from nodecopy - const tempCtx = new Context(); - tempCtx.nextID = ctx.rootContext.nextID; - qweb._compileNode(nodeCopy, tempCtx); - const vars = Object.assign({}, ctx.variables, tempCtx.variables); - ctx.rootContext.nextID = tempCtx.nextID; - // open new scope, if necessary - const hasNewVariables = Object.keys(tempCtx.variables).length > 0; - if (hasNewVariables) { - ctx.addLine("{"); - ctx.indent(); - // add new variables, if any - for (let key in tempCtx.variables) { - const v = tempCtx.variables[key]; - if (v.expr) { - ctx.addLine(`let ${v.id} = ${v.expr};`); - } - // todo: handle XML variables... - } - } - // compile sub template - const subCtx = ctx - .subContext("caller", nodeCopy) - .subContext("variables", Object.create(vars)); - qweb._compileNode(nodeTemplate.elem, subCtx); - // close new scope - if (hasNewVariables) { - ctx.dedent(); - ctx.addLine("}"); - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-foreach - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "foreach", - extraNames: ["as"], - priority: 10, - atNodeEncounter({ node, qweb, ctx }) { - ctx.rootContext.shouldProtectContext = true; - ctx = ctx.subContext("inLoop", true); - const elems = node.getAttribute("t-foreach"); - const name = node.getAttribute("t-as"); - let arrayID = ctx.generateID(); - ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); - ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); - let keysID = ctx.generateID(); - let valuesID = ctx.generateID(); - ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`); - ctx.addIf(`!(_${arrayID} instanceof Array)`); - ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`); - ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`); - ctx.closeIf(); - ctx.addLine(`var _length${keysID} = _${keysID}.length;`); - ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`); - ctx.indent(); - ctx.addLine(`context.${name}_first = i === 0;`); - ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`); - ctx.addLine(`context.${name}_index = i;`); - ctx.addLine(`context.${name} = _${keysID}[i];`); - ctx.addLine(`context.${name}_value = _${valuesID}[i];`); - const nodeCopy = node.cloneNode(true); - let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); - if (!shouldWarn && node.tagName === "t") { - if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) { - shouldWarn = true; - } - if (!shouldWarn && - node.children.length === 1 && - node.children[0].tagName !== "t" && - !node.children[0].hasAttribute("t-key")) { - shouldWarn = true; - } - } - if (shouldWarn) { - console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`); - } - nodeCopy.removeAttribute("t-foreach"); - qweb._compileNode(nodeCopy, ctx); - ctx.dedent(); - ctx.addLine("}"); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-debug - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "debug", - priority: 99, - atNodeEncounter({ ctx }) { - ctx.addLine("debugger;"); - } - }); - //------------------------------------------------------------------------------ - // t-log - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "log", - priority: 99, - atNodeEncounter({ ctx, value }) { - const expr = ctx.formatExpression(value); - ctx.addLine(`console.log(${expr})`); - } - }); - - /** - * Owl QWeb Extensions - * - * This file contains the implementation of non standard QWeb directives, added - * by Owl and that will only work on Owl projects: - * - * - t-on - * - t-ref - * - t-transition - * - t-widget/t-keepalive - * - t-mounted - * - t-slot - */ - //------------------------------------------------------------------------------ - // t-on - //------------------------------------------------------------------------------ - // these are pieces of code that will be injected into the event handler if - // modifiers are specified - const MODS_CODE = { - prevent: "e.preventDefault();", - self: "if (e.target !== this.elm) {return}", - stop: "e.stopPropagation();" - }; - QWeb.addDirective({ - name: "on", - priority: 90, - atNodeCreation({ ctx, fullName, value, nodeID }) { - ctx.rootContext.shouldDefineOwner = true; - const [eventName, ...mods] = fullName.slice(5).split("."); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handlerName = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - ctx.addIf(`!context['${handlerName}']`); - ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`); - ctx.closeIf(); - let params = extraArgs - ? `owner, ${ctx.formatExpression(extraArgs)}` - : "owner"; - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function (mod) { - return MODS_CODE[mod]; - }) - .join(""); - handler += `context['${handlerName}'].call(${params}, e);}`; - } - else { - handler = `context['${handlerName}'].bind(${params})`; - } - if (extraArgs) { - ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); - } - else { - ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } - } - }); - //------------------------------------------------------------------------------ - // t-ref - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "ref", - priority: 95, - atNodeCreation({ ctx, value, addNodeHook }) { - const refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); - addNodeHook("create", `context.refs[${refKey}] = n.elm;`); - } - }); - //------------------------------------------------------------------------------ - // t-transition - //------------------------------------------------------------------------------ - UTILS.nextFrame = function (cb) { - requestAnimationFrame(() => requestAnimationFrame(cb)); - }; - UTILS.transitionInsert = function (vn, name) { - const elm = vn.elm; - // remove potential duplicated vnode that is currently being removed, to - // prevent from having twice the same node in the DOM during an animation - const dup = elm.parentElement && - elm.parentElement.querySelector(`*[data-owl-key='${vn.key}']`); - if (dup) { - dup.remove(); - } - elm.classList.add(name + "-enter"); - elm.classList.add(name + "-enter-active"); - const finalize = () => { - elm.classList.remove(name + "-enter-active"); - elm.classList.remove(name + "-enter-to"); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-enter"); - elm.classList.add(name + "-enter-to"); - whenTransitionEnd(elm, finalize); - }); - }; - UTILS.transitionRemove = function (vn, name, rm) { - const elm = vn.elm; - elm.setAttribute("data-owl-key", vn.key); - elm.classList.add(name + "-leave"); - elm.classList.add(name + "-leave-active"); - const finalize = () => { - elm.classList.remove(name + "-leave-active"); - elm.classList.remove(name + "-leave-to"); - rm(); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-leave"); - elm.classList.add(name + "-leave-to"); - whenTransitionEnd(elm, finalize); - }); - }; - function getTimeout(delays, durations) { - /* istanbul ignore next */ - while (delays.length < durations.length) { - delays = delays.concat(delays); - } - return Math.max.apply(null, durations.map((d, i) => { - return toMs(d) + toMs(delays[i]); - })); - } - // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers - // in a locale-dependent way, using a comma instead of a dot. - // If comma is not replaced with a dot, the input will be rounded down (i.e. acting - // as a floor function) causing unexpected behaviors - function toMs(s) { - return Number(s.slice(0, -1).replace(",", ".")) * 1000; - } - function whenTransitionEnd(elm, cb) { - const styles = window.getComputedStyle(elm); - const delays = (styles.transitionDelay || "").split(", "); - const durations = (styles.transitionDuration || "").split(", "); - const timeout = getTimeout(delays, durations); - if (timeout > 0) { - elm.addEventListener("transitionend", cb, { once: true }); - } - else { - cb(); - } - } - QWeb.addDirective({ - name: "transition", - priority: 96, - atNodeCreation({ value, addNodeHook }) { - let name = value; - const hooks = { - insert: `this.utils.transitionInsert(vn, '${name}');`, - remove: `this.utils.transitionRemove(vn, '${name}', rm);` - }; - for (let hookName in hooks) { - addNodeHook(hookName, hooks[hookName]); - } - } - }); - //------------------------------------------------------------------------------ - // t-widget - //------------------------------------------------------------------------------ - const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, { - self: "if (e.target !== vn.elm) {return}" - }); - /** - * The t-widget directive is certainly a complicated and hard to maintain piece - * of code. To help you, fellow developer, if you have to maintain it, I offer - * you this advice: Good luck... - * - * Since it is not 'direct' code, but rather code that generates other code, it - * is not easy to understand. To help you, here is a detailed and commented - * explanation of the code generated by the t-widget directive for the following - * situation: - * ```xml - * - * ``` - * - * ```js - * // we assign utils on top of the function because it will be useful for - * // each widgets - * let utils = this.utils; - * - * // this is the virtual node representing the parent div - * let c1 = [], p1 = { key: 1 }; - * var vn1 = h("div", p1, c1); - * - * // t-widget directive: we start by evaluating the expression given by t-key: - * let key5 = "somestring"; - * - * // We keep the index of the position of the widget in the closure. We push - * // null to reserve the slot, and will replace it later by the widget vnode, - * // when it will be ready (do not forget that preparing/rendering a widget is - * // asynchronous) - * let _2_index = c1.length; - * c1.push(null); - * - * // def3 is the promise that will contain later either the new widget - * // creation, or the props update... - * let def3; - * - * // this is kind of tricky: we need here to find if the widget was already - * // created by a previous rendering. This is done by checking the internal - * // `cmap` (children map) of the parent widget: it maps keys to widget ids, - * // and, then, if there is an id, we look into the children list to get the - * // instance - * let w4 = - * key5 in context.__owl__.cmap - * ? context.__owl__.children[context.__owl__.cmap[key5]] - * : false; - * - * // we evaluate here the props given to the component. It is done here to be - * // able to easily reference it later, and also, it might be an expensive - * // computation, so it is certainly better to do it only once - * let props4 = { flag: context["state"].flag }; - * - * // If we have a widget, currently rendering, but not ready yet, we do not want - * // to wait for it to be ready if we can avoid it - * if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { - * // we check if the props are the same. In that case, we can simply reuse - * // the previous rendering and skip all useless work - * if (utils.shallowEqual(props4, w4.__owl__.renderProps)) { - * def3 = w4.__owl__.renderPromise; - * } else { - * // if the props are not the same, we destroy the widget and starts anew. - * // this will be faster than waiting for its rendering, then updating it - * w4.destroy(); - * w4 = false; - * } - * } - * - * if (!w4) { - * // in this situation, we need to create a new widget. First step is - * // to get a reference to the class, then create an instance with - * // current context as parent, and the props. - * let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4]; - - * if (!W4) { - * throw new Error("Cannot find the definition of widget 'child'"); - * } - * w4 = new W4(owner, props4); - * - * // Whenever we rerender the parent widget, we need to be sure that we - * // are able to find the widget instance. To do that, we register it to - * // the parent cmap (children map). Note that the 'template' key is - * // used here, since this is what identify the widget from the template - * // perspective. - * context.__owl__.cmap[key5] = w4.__owl__.id; - * - * // _prepare is called, to basically call willStart, then render the - * // widget - * def3 = w4._prepare(); - * - * def3 = def3.then(vnode => { - * // we create here a virtual node for the parent (NOT the widget). This - * // means that the vdom of the parent will be stopped here, and from - * // the parent's perspective, it simply is a vnode with no children. - * // However, it shares the same dom element with the component root - * // vnode. - * let pvnode = h(vnode.sel, { key: key5 }); - * - * // we add hooks to the parent vnode so we can interact with the new - * // widget at the proper time - * pvnode.data.hook = { - * insert(vn) { - * // the _mount method will patch the widget vdom into the elm vn.elm, - * // then call the mounted hooks. However, suprisingly, the snabbdom - * // patch method actually replace the elm by a new elm, so we need - * // to synchronise the pvnode elm with the resulting elm - * let nvn = w4._mount(vnode, vn.elm); - * pvnode.elm = nvn.elm; - * // what follows is only present if there are animations on the widget - * utils.transitionInsert(vn, "fade"); - * }, - * remove() { - * // override with empty function to prevent from removing the node - * // directly. It will be removed when destroy is called anyway, which - * // delays the removal if there are animations. - * }, - * destroy() { - * // if there are animations, we delay the call to destroy on the - * // widget, if not, we call it directly. - * let finalize = () => { - * w4.destroy(); - * }; - * utils.transitionRemove(vn, "fade", finalize); - * } - * }; - * // the pvnode is inserted at the correct position in the div's children - * c1[_2_index] = pvnode; - * - * // we keep here a reference to the parent vnode (representing the - * // widget, so we can reuse it later whenever we update the widget - * w4.__owl__.pvnode = pvnode; - * }); - * } else { - * // this is the 'update' path of the directive. - * // the call to _updateProps is the actual widget update - * // Note that we only update the props if we cannot reuse the previous - * // rendering work (in the case it was rendered with the same props) - * def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); - * def3 = def3.then(() => { - * // if widget was destroyed in the meantime, we do nothing (so, this - * // means that the parent's element children list will have a null in - * // the widget's position, which will cause the pvnode to be removed - * // when it is patched. - * if (w4.__owl__.isDestroyed) { - * return; - * } - * // like above, we register the pvnode to the children list, so it - * // will not be patched out of the dom. - * let pvnode = w4.__owl__.pvnode; - * c1[_2_index] = pvnode; - * }); - * } - * - * // we register the deferred here so the parent can coordinate its patch operation - * // with all the children. - * extra.promises.push(def3); - * return vn1; - * ``` - */ - QWeb.addDirective({ - name: "widget", - extraNames: ["props", "keepalive"], - priority: 100, - atNodeEncounter({ ctx, value, node, qweb }) { - ctx.addLine("//WIDGET"); - ctx.rootContext.shouldDefineOwner = true; - ctx.rootContext.shouldDefineQWeb = true; - ctx.rootContext.shouldDefineUtils = true; - let keepAlive = node.getAttribute("t-keepalive") ? true : false; - // t-on- events and t-transition - const events = []; - let transition = ""; - const attributes = node.attributes; - const props = {}; - for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; - const value = attributes[i].textContent; - if (name.startsWith("t-on-")) { - const [eventName, ...mods] = name.slice(5).split("."); - let extraArgs; - let handlerName = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - events.push([eventName, mods, handlerName, extraArgs]); - } - else if (name === "t-transition") { - transition = value; - } - else if (!name.startsWith("t-")) { - if (name !== "class" && name !== "style") { - // this is a prop! - props[name] = ctx.formatExpression(value); - } - } - } - let key = node.getAttribute("t-key"); - if (key) { - key = ctx.formatExpression(key); - } - // computing the props string representing the props object - let propStr = Object.keys(props) - .map(k => k + ":" + props[k]) - .join(","); - let dummyID = ctx.generateID(); - let defID = ctx.generateID(); - let widgetID = ctx.generateID(); - let keyID = key && ctx.generateID(); - if (key) { - // we bind a variable to the key (could be a complex expression, so we - // want to evaluate it only once) - ctx.addLine(`let key${keyID} = ${key};`); - } - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); - ctx.addLine(`c${ctx.parentNode}.push(null);`); - ctx.addLine(`let def${defID};`); - let templateID = key - ? `key${keyID}` - : ctx.inLoop - ? `String(-${widgetID} - i)` - : String(widgetID); - let ref = node.getAttribute("t-ref"); - let refExpr = ""; - let refKey = ""; - if (ref) { - refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); - refExpr = `context.refs[${refKey}] = w${widgetID};`; - } - let transitionsInsertCode = ""; - if (transition) { - transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`; - } - let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}();`; - if (ref && !keepAlive) { - finalizeWidgetCode += `delete context.refs[${refKey}];`; - } - if (transition) { - finalizeWidgetCode = `let finalize = () => { - ${finalizeWidgetCode} - }; - utils.transitionRemove(vn, '${transition}', finalize);`; - } - let createHook = ""; - let classAttr = node.getAttribute("class"); - let tattClass = node.getAttribute("t-att-class"); - let styleAttr = node.getAttribute("style"); - let tattStyle = node.getAttribute("t-att-style"); - if (tattStyle) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`); - tattStyle = attVar; - } - let updateClassCode = ""; - if (classAttr || tattClass || styleAttr || tattStyle || events.length) { - let classCode = ""; - if (classAttr) { - classCode = - classAttr - .split(" ") - .map(c => `vn.elm.classList.add('${c}')`) - .join(";") + ";"; - } - if (tattClass) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`); - classCode = `for (let k in ${attVar}) { - if (${attVar}[k]) { - vn.elm.classList.add(k); - } - }`; - updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; - } - let eventsCode = events - .map(function ([eventName, mods, handlerName, extraArgs]) { - let params = extraArgs - ? `owner, ${ctx.formatExpression(extraArgs)}` - : "owner"; - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function (mod) { - return T_WIDGET_MODS_CODE[mod]; - }) - .join(""); - handler += `owner['${handlerName}'].call(${params}, e);}`; - } - else { - handler = `owner['${handlerName}'].bind(${params})`; - } - return `vn.elm.addEventListener('${eventName}', ${handler});`; - }) - .join(""); - const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); - const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : ""; - createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`; - } - ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); - ctx.addLine(`let props${widgetID} = {${propStr}};`); - ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`); - ctx.addIf(`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`); - ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`); - ctx.addElse(); - ctx.addLine(`w${widgetID}.destroy();`); - ctx.addLine(`w${widgetID} = false;`); - ctx.closeIf(); - ctx.closeIf(); - ctx.addIf(`!w${widgetID}`); - // new widget - ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`); - ctx.addLine(`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`); - // maybe only do this in dev mode... - ctx.addLine(`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`); - ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`); - ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`); - // SLOTS - const slotNodes = node.querySelectorAll("[t-set]"); - if (slotNodes.length) { - const slotId = qweb.nextSlotId++; - for (let i = 0, length = slotNodes.length; i < length; i++) { - const slotNode = slotNodes[i]; - const key = slotNode.getAttribute("t-set"); - slotNode.removeAttribute("t-set"); - const slotFn = qweb._compile(`slot_${key}_template`, slotNode); - qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb); - } - ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`); - } - ctx.addLine(`def${defID} = w${widgetID}._prepare();`); - // hack: specify empty remove hook to prevent the node from being removed from the DOM - // FIXME: click to re-add widget during remove transition -> leak - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`); - ctx.addElse(); - // need to update widget - ctx.addLine(`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`); - let keepAliveCode = ""; - if (keepAlive) { - keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`; - } - ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`); - ctx.closeIf(); - ctx.addLine(`extra.promises.push(def${defID});`); - if (node.hasAttribute("t-if") || - node.hasAttribute("t-else") || - node.hasAttribute("t-elif")) { - ctx.closeIf(); - } - return true; - } - }); - //------------------------------------------------------------------------------ - // t-mounted - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "mounted", - priority: 97, - atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) { - ctx.rootContext.shouldDefineOwner = true; - const eventName = fullName.slice(5); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handler = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`; - if (extraArgs) { - ctx.addLine(`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`); - } - else { - ctx.addLine(`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`); - } - addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`); - } - }); - //------------------------------------------------------------------------------ - // t-slot - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "slot", - priority: 80, - atNodeEncounter({ ctx, value }) { - const slotKey = ctx.generateID(); - ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); - ctx.addLine(`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`); - return true; - } - }); - - class Store extends EventBus { - constructor(config, options = {}) { - super(); - this._commitLevel = 0; - this.history = []; - this.debug = options.debug || false; - this.state = config.state || {}; - this.actions = config.actions; - this.mutations = config.mutations; - this.env = config.env; - this.observer = new Observer(); - this.observer.notifyCB = this.trigger.bind(this, "update"); - this.observer.allowMutations = false; - this.observer.observe(this.state); - this.getters = {}; - if (this.debug) { - this.history.push({ state: this.state }); - } - for (let entry of Object.entries(config.getters || {})) { - const name = entry[0]; - const func = entry[1]; - this.getters[name] = payload => { - return func({ state: this.state, getters: this.getters }, payload); - }; - } - } - dispatch(action, payload) { - if (!this.actions[action]) { - throw new Error(`[Error] action ${action} is undefined`); - } - const result = this.actions[action]({ - commit: this.commit.bind(this), - dispatch: this.dispatch.bind(this), - env: this.env, - state: this.state, - getters: this.getters - }, payload); - if (result instanceof Promise) { - return new Promise((resolve, reject) => { - result.then(() => resolve()); - result.catch(reject); - }); - } - } - commit(type, payload) { - if (!this.mutations[type]) { - throw new Error(`[Error] mutation ${type} is undefined`); - } - this._commitLevel++; - this.observer.allowMutations = true; - const res = this.mutations[type].call(null, { - commit: this.commit.bind(this), - state: this.state, - getters: this.getters - }, payload); - if (this._commitLevel === 1) { - this.observer.allowMutations = false; - if (this.debug) { - this.history.push({ - state: this.state, - mutation: type, - payload: payload - }); - } - } - this._commitLevel--; - return res; - } - } - //------------------------------------------------------------------------------ - // Connect function - //------------------------------------------------------------------------------ - function revNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.rev; - } - return 0; - } - function deepRevNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.deepRev; - } - return 0; - } - let nextID$1 = 1; - function connect(Comp, mapStoreToProps, options = {}) { - let hashFunction = options.hashFunction || null; - const getStore = options.getStore || (env => env.store); - if (!hashFunction) { - let deep = "deep" in options ? options.deep : true; - let defaultRevFunction = deep ? deepRevNumber : revNumber; - hashFunction = function ({ storeProps }, options) { - const { currentStoreProps } = options; - if ("__owl__" in storeProps) { - return defaultRevFunction(storeProps); - } - let hash = 0; - for (let key in storeProps) { - const val = storeProps[key]; - const hashVal = defaultRevFunction(val); - if (hashVal === 0) { - if (val !== currentStoreProps[key]) { - options.didChange = true; - } - } - else { - hash += hashVal; - } - } - return hash; - }; - } - const Result = class extends Comp { - constructor(parent, props) { - const env = parent instanceof Component ? parent.env : parent; - const store = getStore(env); - const ownProps = Object.assign({}, props || {}); - const storeProps = mapStoreToProps(store.state, ownProps, store.getters); - const mergedProps = Object.assign({}, props || {}, storeProps); - super(parent, mergedProps); - this.__owl__.ownProps = ownProps; - this.__owl__.currentStoreProps = storeProps; - this.__owl__.store = store; - this.__owl__.storeHash = hashFunction({ - state: store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, { - currentStoreProps: storeProps - }); - } - /** - * We do not use the mounted hook here for a subtle reason: we want the - * updates to be called for the parents before the children. However, - * if we use the mounted hook, this will be done in the reverse order. - */ - _callMounted() { - this.__owl__.store.on("update", this, this._checkUpdate); - super._callMounted(); - } - willUnmount() { - this.__owl__.store.off("update", this); - super.willUnmount(); - } - _checkUpdate() { - const ownProps = this.__owl__.ownProps; - const storeProps = mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters); - const options = { - currentStoreProps: this.__owl__.currentStoreProps - }; - const storeHash = hashFunction({ - state: this.__owl__.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, options); - let didChange = options.didChange; - if (storeHash !== this.__owl__.storeHash) { - didChange = true; - this.__owl__.storeHash = storeHash; - } - if (didChange) { - this.__owl__.currentStoreProps = storeProps; - this._updateProps(ownProps, false); - } - } - _updateProps(nextProps, forceUpdate, patchQueue) { - if (this.__owl__.ownProps !== nextProps) { - this.__owl__.currentStoreProps = mapStoreToProps(this.__owl__.store.state, nextProps, this.__owl__.store.getters); - } - this.__owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps); - return super._updateProps(mergedProps, forceUpdate, patchQueue); - } - }; - // we assign here a unique name to the resulting anonymous class. - // this is necessary for Owl to be able to properly deduce templates. - // Otherwise, all connected components would have the same name, and then - // each component after the first will necessarily have the same template. - let name = `ConnectedComponent${nextID$1++}`; - Object.defineProperty(Result, "name", { value: name }); - return Result; - } - - /** - * Owl Utils - * - * We have here a small collection of utility functions: - * - * - whenReady - * - loadJS - * - loadTemplates - * - escape - * - debounce - */ - function whenReady(fn) { - return new Promise(function (resolve) { - if (document.readyState !== "loading") { - resolve(); - } - else { - document.addEventListener("DOMContentLoaded", resolve, false); - } - }).then(fn || function () { }); - } - const loadedScripts = {}; - function loadJS(url) { - if (url in loadedScripts) { - return loadedScripts[url]; - } - const promise = new Promise(function (resolve, reject) { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.src = url; - script.onload = function () { - resolve(); - }; - script.onerror = function () { - reject(`Error loading file '${url}'`); - }; - const head = document.head || document.getElementsByTagName("head")[0]; - head.appendChild(script); - }); - loadedScripts[url] = promise; - return promise; - } - async function loadTemplates(url) { - const result = await fetch(url); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); - } - let templates = await result.text(); - templates = templates.replace(//g, ""); - return templates; - } - function escape(str) { - if (str === undefined) { - return ""; - } - if (typeof str === "number") { - return String(str); - } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, "'") - .replace(/`/g, "`"); - } - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. - * - * Inspired by https://davidwalsh.name/javascript-debounce-function - */ - function debounce(func, wait, immediate) { - let timeout; - return function () { - const context = this; - const args = arguments; - function later() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - } - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - }; - } - - var _utils = /*#__PURE__*/Object.freeze({ - whenReady: whenReady, - loadJS: loadJS, - loadTemplates: loadTemplates, - escape: escape, - debounce: debounce - }); - - /** - * This file is the main file packaged by rollup (see rollup.config.js). From - * this file, we export all public owl elements. - * - * Note that dynamic values, such as a date or a commit hash are added by rollup - */ - const __info__ = {}; - Object.defineProperty(__info__, "mode", { - get() { - return QWeb.dev ? "dev" : "prod"; - }, - set(mode) { - QWeb.dev = mode === "dev"; - if (QWeb.dev) { - const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`; - console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`); - } - else { - console.log(`Owl is now running in 'prod' mode.`); - } - } - }); - const utils = _utils; - - exports.Component = Component; - exports.EventBus = EventBus; - exports.Observer = Observer; - exports.QWeb = QWeb; - exports.Store = Store; - exports.__info__ = __info__; - exports.connect = connect; - exports.utils = utils; - - exports.__info__.version = '0.14.0'; - exports.__info__.date = '2019-06-13T10:11:15.338Z'; - exports.__info__.hash = '2701a7d'; - exports.__info__.url = 'https://github.com/odoo/owl'; - -}(this.owl = this.owl || {})); diff --git a/tools/benchmarks/owl-0.14.0/templates.xml b/tools/benchmarks/owl-0.14.0/templates.xml deleted file mode 100644 index b53a3dac..00000000 --- a/tools/benchmarks/owl-0.14.0/templates.xml +++ /dev/null @@ -1,50 +0,0 @@ - -
-
-
Actions
-
- - - - - - -
-
-
- - -
-
- - -
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
- - - -
-
-
- -
- - - - -
- -
- -
- - \ No newline at end of file diff --git a/tools/benchmarks/owl-0.16.0/app.js b/tools/benchmarks/owl-0.16.0/app.js deleted file mode 100644 index 945b1443..00000000 --- a/tools/benchmarks/owl-0.16.0/app.js +++ /dev/null @@ -1,161 +0,0 @@ -import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js"; - -//------------------------------------------------------------------------------ -// Likes Counter Widget -//------------------------------------------------------------------------------ -class Counter extends owl.Component { - state = { counter: 0 }; - - increment() { - this.state.counter++; - } -} - -//------------------------------------------------------------------------------ -// Message Widget -//------------------------------------------------------------------------------ -class Message extends owl.Component { - components = { Counter }; - - shouldUpdate(nextProps) { - return nextProps.message !== this.props.message; - } - removeMessage() { - this.trigger("remove-message", { - id: this.props.message.id - }); - } -} - -//------------------------------------------------------------------------------ -// Root Widget -//------------------------------------------------------------------------------ -class App extends owl.Component { - components = { Message }; - state = { messages: [], multipleFlag: false, clearAfterFlag: false }; - - mounted() { - this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); - } - - benchmark(message, fn, callback) { - if (this.state.multipleFlag) { - const N = 20; - let n = N; - let total = 0; - let cb = info => { - let finalize = () => { - n--; - total += info.delta; - if (n === 0) { - const avg = total / N; - this.log(`Average: ${formatNumber(avg)}ms`, true); - if (callback) { - callback(); - } - } else { - this._benchmark(message, fn, cb); - } - }; - - if (this.state.clearAfterFlag) { - this._benchmark( - "clear", - () => { - this.state.messages = []; - }, - finalize, - false - ); - } else { - finalize(); - } - }; - this._benchmark(message, fn, cb); - } else { - this._benchmark(message, fn, callback); - } - } - - _benchmark(message, fn, cb, log = true) { - setTimeout(() => { - startMeasure(message); - fn(); - stopMeasure(info => { - if (log) { - this.log(info.msg); - } - if (cb) { - cb(info); - } - }); - }, 10); - } - - addMessages(n) { - this.benchmark("add " + n, () => { - const newMessages = buildData(n); - this.state.messages.push.apply(this.state.messages, newMessages); - }); - } - - clear() { - this._benchmark("clear", () => { - this.state.messages = []; - }); - } - - updateSomeMessages() { - this.benchmark("update every 10th", () => { - const messages = this.state.messages; - for (let i = 0; i < this.state.messages.length; i += 10) { - const msg = Object.assign({}, messages[i]); - msg.author += "!!!"; - this.set(messages, i, msg); - } - }); - } - - removeMessage(event) { - this.benchmark("remove message", () => { - const index = this.state.messages.findIndex(m => m.id === event.detail.id); - this.state.messages.splice(index, 1); - }); - } - - log(str, isBold) { - const div = document.createElement("div"); - if (isBold) { - div.classList.add("bold"); - } - div.textContent = `> ${str}`; - this.refs.log.appendChild(div); - this.refs.log.scrollTop = this.refs.log.scrollHeight; - } - - clearLog() { - this.refs.log.innerHTML = ""; - } - - toggleMultiple() { - this.state.multipleFlag = !this.state.multipleFlag; - } - - toggleClear() { - this.state.clearAfterFlag = !this.state.clearAfterFlag; - } -} - -//------------------------------------------------------------------------------ -// Application initialization -//------------------------------------------------------------------------------ -async function start() { - const templates = await owl.utils.loadTemplates("templates.xml"); - const env = { - qweb: new owl.QWeb(templates) - }; - const app = new App(env); - app.mount(document.body); -} - -start(); diff --git a/tools/benchmarks/owl-0.16.0/index.html b/tools/benchmarks/owl-0.16.0/index.html deleted file mode 100644 index 23e1f555..00000000 --- a/tools/benchmarks/owl-0.16.0/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - OWL 0.16.0 Benchmark - - - - -
- - - diff --git a/tools/benchmarks/owl-0.8.0/app.js b/tools/benchmarks/owl-0.8.0/app.js deleted file mode 100644 index f89a020a..00000000 --- a/tools/benchmarks/owl-0.8.0/app.js +++ /dev/null @@ -1,173 +0,0 @@ -import { - buildData, - startMeasure, - stopMeasure, - formatNumber -} from "../shared/utils.js"; - -//------------------------------------------------------------------------------ -// Likes Counter Widget -//------------------------------------------------------------------------------ -class Counter extends owl.Component { - state = { counter: 0 }; - template = "Counter"; - - increment() { - this.state.counter++; - } -} - -//------------------------------------------------------------------------------ -// Message Widget -//------------------------------------------------------------------------------ -class Message extends owl.Component { - widgets = { Counter }; - template = "Message"; - - shouldUpdate(nextProps) { - return nextProps !== this.props; - } - removeMessage() { - this.trigger("remove_message", { - id: this.props.id - }); - } -} - -//------------------------------------------------------------------------------ -// Root Widget -//------------------------------------------------------------------------------ -class App extends owl.Component { - widgets = { Message }; - state = { messages: [], multipleFlag: false, clearAfterFlag: false }; - template = "App"; - - mounted() { - this.log( - `Benchmarking Owl v${owl._version} (build date: ${ - owl._date - })` - ); - } - - benchmark(message, fn, callback) { - if (this.state.multipleFlag) { - const N = 20; - let n = N; - let total = 0; - let cb = info => { - let finalize = () => { - n--; - total += info.delta; - if (n === 0) { - const avg = total / N; - this.log(`Average: ${formatNumber(avg)}ms`, true); - if (callback) { - callback(); - } - } else { - this._benchmark(message, fn, cb); - } - }; - - if (this.state.clearAfterFlag) { - this._benchmark( - "clear", - () => { - this.state.messages = []; - }, - finalize, - false - ); - } else { - finalize(); - } - }; - this._benchmark(message, fn, cb); - } else { - this._benchmark(message, fn, callback); - } - } - - _benchmark(message, fn, cb, log = true) { - setTimeout(() => { - startMeasure(message); - fn(); - stopMeasure(info => { - if (log) { - this.log(info.msg); - } - if (cb) { - cb(info); - } - }); - }, 10); - } - - addMessages(n) { - this.benchmark("add " + n, () => { - const newMessages = buildData(n); - this.state.messages.push.apply(this.state.messages, newMessages); - }); - } - - clear() { - this._benchmark("clear", () => { - this.state.messages = []; - }); - } - - updateSomeMessages() { - this.benchmark("update every 10th", () => { - const messages = this.state.messages; - for (let i = 0; i < this.state.messages.length; i += 10) { - const msg = Object.assign({}, messages[i]); - msg.author += "!!!"; - this.set(messages, i, msg); - } - }); - } - - removeMessage(data) { - this.benchmark("remove message", () => { - const index = this.state.messages.findIndex(m => m.id === data.id); - this.state.messages.splice(index, 1); - }); - } - - log(str, isBold) { - const div = document.createElement("div"); - if (isBold) { - div.classList.add("bold"); - } - div.textContent = `> ${str}`; - this.refs.log.appendChild(div); - this.refs.log.scrollTop = this.refs.log.scrollHeight; - } - - clearLog() { - this.refs.log.innerHTML = ""; - } - - toggleMultiple() { - this.state.multipleFlag = !this.state.multipleFlag; - } - - toggleClear() { - this.state.clearAfterFlag = !this.state.clearAfterFlag; - } -} - -//------------------------------------------------------------------------------ -// Application initialization -//------------------------------------------------------------------------------ -async function start() { - const templates = await owl.utils.loadTemplates("templates.xml"); - const env = { - qweb: new owl.QWeb(templates) - }; - const app = new App(env); - app.mount(document.body); -} - -start(); diff --git a/tools/benchmarks/owl-0.8.0/index.html b/tools/benchmarks/owl-0.8.0/index.html deleted file mode 100644 index 1e635556..00000000 --- a/tools/benchmarks/owl-0.8.0/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - OWL 0.8.0 Benchmark - - - - -
- - - diff --git a/tools/benchmarks/owl-0.8.0/owl-0.8.0.js b/tools/benchmarks/owl-0.8.0/owl-0.8.0.js deleted file mode 100644 index 2877ab50..00000000 --- a/tools/benchmarks/owl-0.8.0/owl-0.8.0.js +++ /dev/null @@ -1,2414 +0,0 @@ -(function (exports) { - 'use strict'; - - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in some cases. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a widget or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let sub of subs) { - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } - - //------------------------------------------------------------------------------ - // Observer - //------------------------------------------------------------------------------ - const methodsToPatch = [ - "push", - "pop", - "shift", - "unshift", - "splice", - "sort", - "reverse" - ]; - const ArrayProto = Array.prototype; - const ModifiedArrayProto = Object.create(ArrayProto); - for (let method of methodsToPatch) { - const initialMethod = ArrayProto[method]; - ModifiedArrayProto[method] = function (...args) { - if (!this.__observer__.allowMutations) { - throw new Error(`Array cannot be changed here")`); - } - this.__observer__.notifyChange(); - this.__owl__.rev++; - let parent = this; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent)); - let inserted; - switch (method) { - case "push": - case "unshift": - inserted = args; - break; - case "splice": - inserted = args.slice(2); - break; - } - if (inserted) { - for (let elem of inserted) { - this.__observer__.observe(elem, this); - } - } - return initialMethod.call(this, ...args); - }; - } - class Observer { - constructor() { - this.rev = 1; - this.allowMutations = true; - this.dirty = false; - } - notifyCB() { } - notifyChange() { - this.rev++; - this.dirty = true; - Promise.resolve().then(() => { - if (this.dirty) { - this.dirty = false; - this.notifyCB(); - } - }); - } - observe(value, parent) { - if (value === null) { - // fun fact: typeof null === 'object' - return; - } - if (typeof value !== "object") { - return; - } - if ("__owl__" in value) { - // already observed - value.__owl__.parent = parent; - return; - } - if (Array.isArray(value)) { - this._observeArr(value, parent); - } - else { - this._observeObj(value, parent); - } - } - set(target, key, value) { - this._addProp(target, key, value); - target.__owl__.rev++; - this.notifyChange(); - } - _observeObj(obj, parent) { - const keys = Object.keys(obj); - obj.__owl__ = { rev: 1, deepRev: 1, parent }; - Object.defineProperty(obj, "__owl__", { enumerable: false }); - for (let key of keys) { - this._addProp(obj, key, obj[key]); - } - } - _observeArr(arr, parent) { - arr.__owl__ = { rev: 1, deepRev: 1, parent }; - Object.defineProperty(arr, "__owl__", { enumerable: false }); - arr.__proto__ = Object.create(ModifiedArrayProto); - arr.__proto__.__observer__ = this; - for (let i = 0; i < arr.length; i++) { - this.observe(arr[i], arr); - } - } - _addProp(obj, key, value) { - var self = this; - Object.defineProperty(obj, key, { - enumerable: true, - get() { - return value; - }, - set(newVal) { - if (newVal !== value) { - if (!self.allowMutations) { - throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); - } - value = newVal; - self.observe(newVal, obj); - obj.__owl__.rev++; - let parent = obj; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent) && parent !== obj); - self.notifyChange(); - } - } - }); - this.observe(value, obj); - } - } - - function escape(str) { - if (str === undefined) { - return ""; - } - if (typeof str === "number") { - return String(str); - } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, "'") - .replace(/`/g, "`"); - } - /** - * Remove trailing and leading spaces - */ - function htmlTrim(s) { - let result = s.replace(/(^\s+|\s+$)/g, ""); - if (s[0] === " ") { - result = " " + result; - } - if (result !== " " && s[s.length - 1] === " ") { - result = result + " "; - } - return result; - } - /** - * Create a function that will generate unique id numbers - */ - function idGenerator() { - let nextID = 1; - return () => nextID++; - } - function memoize(f, hash) { - if (!hash) { - hash = args => args.map(a => String(a)).join(","); - } - let cache = {}; - function memoizedFunction(...args) { - let hashValue = hash(args); - if (!(hashValue in cache)) { - cache[hashValue] = f(...args); - } - return cache[hashValue]; - } - return memoizedFunction; - } - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. - * - * Inspired by https://davidwalsh.name/javascript-debounce-function - */ - function debounce(func, wait, immediate) { - let timeout; - return function () { - const context = this; - const args = arguments; - function later() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - } - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - }; - } - /** - * Find a node in a tree. - * - * This will traverse the tree (depth first) and return the first child that - * matches the predicate, if any - */ - function findInTree(tree, predicate) { - if (predicate(tree)) { - return tree; - } - for (let child of tree.children) { - let match = findInTree(child, predicate); - if (match) { - return match; - } - } - return null; - } - function patch(C, patchName, patch) { - const proto = C.prototype; - if (!proto.__patches) { - proto.__patches = { - origMethods: {}, - patches: {}, - current: [] - }; - } - if (proto.__patches.patches[patchName]) { - throw new Error(`Patch [${patchName}] already exists`); - } - proto.__patches.patches[patchName] = patch; - applyPatch(proto, patch); - proto.__patches.current.push(patchName); - function applyPatch(proto, patch) { - Object.keys(patch).forEach(function (methodName) { - const method = patch[methodName]; - if (typeof method === "function") { - const original = proto[methodName]; - if (!(methodName in proto.__patches.origMethods)) { - proto.__patches.origMethods[methodName] = original; - } - proto[methodName] = function (...args) { - this._super = original; - return method.call(this, ...args); - }; - } - }); - } - } - function unpatch(C, patchName) { - const proto = C.prototype; - const patchInfo = proto.__patches; - delete proto.__patches; - // reset to original - for (let k in patchInfo.origMethods) { - proto[k] = patchInfo.origMethods[k]; - } - // apply other patches - for (let name of patchInfo.current) { - if (name !== patchName) { - patch(C, name, patchInfo.patches[name]); - } - } - } - async function loadTemplates(url) { - const result = await fetch(url); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); - } - let templates = await result.text(); - templates = templates.replace(//g, ""); - return templates; - } - - var _utils = /*#__PURE__*/Object.freeze({ - escape: escape, - htmlTrim: htmlTrim, - idGenerator: idGenerator, - memoize: memoize, - debounce: debounce, - findInTree: findInTree, - patch: patch, - unpatch: unpatch, - loadTemplates: loadTemplates - }); - - function vnode(sel, data, children, text, elm) { - let key = data === undefined ? undefined : data.key; - return { - sel: sel, - data: data, - children: children, - text: text, - elm: elm, - key: key - }; - } - //------------------------------------------------------------------------------ - // snabbdom.ts - //------------------------------------------------------------------------------ - function isUndef(s) { - return s === undefined; - } - function isDef(s) { - return s !== undefined; - } - const emptyNode = vnode("", {}, [], undefined, undefined); - function sameVnode(vnode1, vnode2) { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; - } - function isVnode(vnode) { - return vnode.sel !== undefined; - } - function createKeyToOldIdx(children, beginIdx, endIdx) { - let i, map = {}, key, ch; - for (i = beginIdx; i <= endIdx; ++i) { - ch = children[i]; - if (ch != null) { - key = ch.key; - if (key !== undefined) - map[key] = i; - } - } - return map; - } - const hooks = [ - "create", - "update", - "remove", - "destroy", - "pre", - "post" - ]; - function init(modules, domApi) { - let i, j, cbs = {}; - const api = domApi !== undefined ? domApi : htmlDomApi; - for (i = 0; i < hooks.length; ++i) { - cbs[hooks[i]] = []; - for (j = 0; j < modules.length; ++j) { - const hook = modules[j][hooks[i]]; - if (hook !== undefined) { - cbs[hooks[i]].push(hook); - } - } - } - function emptyNodeAt(elm) { - const id = elm.id ? "#" + elm.id : ""; - const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; - return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - function createRmCb(childElm, listeners) { - return function rmCb() { - if (--listeners === 0) { - const parent = api.parentNode(childElm); - api.removeChild(parent, childElm); - } - }; - } - function createElm(vnode, insertedVnodeQueue) { - let i, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.init))) { - i(vnode); - data = vnode.data; - } - } - let children = vnode.children, sel = vnode.sel; - if (sel === "!") { - if (isUndef(vnode.text)) { - vnode.text = ""; - } - vnode.elm = api.createComment(vnode.text); - } - else if (sel !== undefined) { - // Parse selector - const hashIdx = sel.indexOf("#"); - const dotIdx = sel.indexOf(".", hashIdx); - const hash = hashIdx > 0 ? hashIdx : sel.length; - const dot = dotIdx > 0 ? dotIdx : sel.length; - const tag = hashIdx !== -1 || dotIdx !== -1 - ? sel.slice(0, Math.min(hash, dot)) - : sel; - const elm = (vnode.elm = - isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, tag) - : api.createElement(tag)); - if (hash < dot) - elm.setAttribute("id", sel.slice(hash + 1, dot)); - if (dotIdx > 0) - elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); - for (i = 0; i < cbs.create.length; ++i) - cbs.create[i](emptyNode, vnode); - if (array(children)) { - for (i = 0; i < children.length; ++i) { - const ch = children[i]; - if (ch != null) { - api.appendChild(elm, createElm(ch, insertedVnodeQueue)); - } - } - } - else if (primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) - i.create(emptyNode, vnode); - if (i.insert) - insertedVnodeQueue.push(vnode); - } - } - else { - vnode.elm = api.createTextNode(vnode.text); - } - return vnode.elm; - } - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - if (ch != null) { - api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); - } - } - } - function invokeDestroyHook(vnode) { - let i, j, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.destroy))) - i(vnode); - for (i = 0; i < cbs.destroy.length; ++i) - cbs.destroy[i](vnode); - if (vnode.children !== undefined) { - for (j = 0; j < vnode.children.length; ++j) { - i = vnode.children[j]; - if (i != null && typeof i !== "string") { - invokeDestroyHook(i); - } - } - } - } - } - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - let i, listeners, rm, ch = vnodes[startIdx]; - if (ch != null) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (i = 0; i < cbs.remove.length; ++i) - cbs.remove[i](ch, rm); - if (isDef((i = ch.data)) && - isDef((i = i.hook)) && - isDef((i = i.remove))) { - i(ch, rm); - } - else { - rm(); - } - } - else { - // Text node - api.removeChild(parentElm, ch.elm); - } - } - } - } - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - let oldStartIdx = 0, newStartIdx = 0; - let oldEndIdx = oldCh.length - 1; - let oldStartVnode = oldCh[0]; - let oldEndVnode = oldCh[oldEndIdx]; - let newEndIdx = newCh.length - 1; - let newStartVnode = newCh[0]; - let newEndVnode = newCh[newEndIdx]; - let oldKeyToIdx; - let idxInOld; - let elmToMove; - let before; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left - } - else if (oldEndVnode == null) { - oldEndVnode = oldCh[--oldEndIdx]; - } - else if (newStartVnode == null) { - newStartVnode = newCh[++newStartIdx]; - } - else if (newEndVnode == null) { - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newEndVnode)) { - // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldEndVnode, newStartVnode)) { - // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { - // New element - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - else { - elmToMove = oldCh[idxInOld]; - if (elmToMove.sel !== newStartVnode.sel) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } - else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - } - newStartVnode = newCh[++newStartIdx]; - } - } - } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } - else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - } - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - let i, hook; - if (isDef((i = vnode.data)) && - isDef((hook = i.hook)) && - isDef((i = hook.prepatch))) { - i(oldVnode, vnode); - } - const elm = (vnode.elm = oldVnode.elm); - let oldCh = oldVnode.children; - let ch = vnode.children; - if (oldVnode === vnode) - return; - if (vnode.data !== undefined) { - for (i = 0; i < cbs.update.length; ++i) - cbs.update[i](oldVnode, vnode); - i = vnode.data.hook; - if (isDef(i) && isDef((i = i.update))) - i(oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) - updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } - else if (isDef(ch)) { - if (isDef(oldVnode.text)) - api.setTextContent(elm, ""); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } - else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ""); - } - } - else if (oldVnode.text !== vnode.text) { - if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - api.setTextContent(elm, vnode.text); - } - if (isDef(hook) && isDef((i = hook.postpatch))) { - i(oldVnode, vnode); - } - } - return function patch(oldVnode, vnode) { - let i, elm, parent; - const insertedVnodeQueue = []; - for (i = 0; i < cbs.pre.length; ++i) - cbs.pre[i](); - if (!isVnode(oldVnode)) { - oldVnode = emptyNodeAt(oldVnode); - } - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } - else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - createElm(vnode, insertedVnodeQueue); - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - for (i = 0; i < insertedVnodeQueue.length; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0; i < cbs.post.length; ++i) - cbs.post[i](); - return vnode; - }; - } - //------------------------------------------------------------------------------ - // is.ts - //------------------------------------------------------------------------------ - const array = Array.isArray; - function primitive(s) { - return typeof s === "string" || typeof s === "number"; - } - function createElement(tagName) { - return document.createElement(tagName); - } - function createElementNS(namespaceURI, qualifiedName) { - return document.createElementNS(namespaceURI, qualifiedName); - } - function createTextNode(text) { - return document.createTextNode(text); - } - function createComment(text) { - return document.createComment(text); - } - function insertBefore(parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); - } - function removeChild(node, child) { - node.removeChild(child); - } - function appendChild(node, child) { - node.appendChild(child); - } - function parentNode(node) { - return node.parentNode; - } - function nextSibling(node) { - return node.nextSibling; - } - function tagName(elm) { - return elm.tagName; - } - function setTextContent(node, text) { - node.textContent = text; - } - function getTextContent(node) { - return node.textContent; - } - function isElement(node) { - return node.nodeType === 1; - } - function isText(node) { - return node.nodeType === 3; - } - function isComment(node) { - return node.nodeType === 8; - } - const htmlDomApi = { - createElement, - createElementNS, - createTextNode, - createComment, - insertBefore, - removeChild, - appendChild, - parentNode, - nextSibling, - tagName, - setTextContent, - getTextContent, - isElement, - isText, - isComment - }; - function addNS(data, children, sel) { - data.ns = "http://www.w3.org/2000/svg"; - if (sel !== "foreignObject" && children !== undefined) { - for (let i = 0; i < children.length; ++i) { - let childData = children[i].data; - if (childData !== undefined) { - addNS(childData, children[i].children, children[i].sel); - } - } - } - } - function h(sel, b, c) { - var data = {}, children, text, i; - if (c !== undefined) { - data = b; - if (array(c)) { - children = c; - } - else if (primitive(c)) { - text = c; - } - else if (c && c.sel) { - children = [c]; - } - } - else if (b !== undefined) { - if (array(b)) { - children = b; - } - else if (primitive(b)) { - text = b; - } - else if (b && b.sel) { - children = [b]; - } - else { - data = b; - } - } - if (children !== undefined) { - for (i = 0; i < children.length; ++i) { - if (primitive(children[i])) - children[i] = vnode(undefined, undefined, undefined, children[i], undefined); - } - } - if (sel[0] === "s" && - sel[1] === "v" && - sel[2] === "g" && - (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { - addNS(data, children, sel); - } - return vnode(sel, data, children, text, undefined); - } - function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; - if (!oldProps && !props) - return; - if (oldProps === props) - return; - oldProps = oldProps || {}; - props = props || {}; - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - const propsModule = { - create: updateProps, - update: updateProps - }; - function invokeHandler(handler, vnode, event) { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } - else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); - } - else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); - } - } - else { - // call multiple handlers - for (var i = 0; i < handler.length; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } - } - function handleEvent(event, vnode) { - var name = event.type, on = vnode.data.on; - // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } - } - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - oldElm.removeEventListener(name, oldListener, false); - } - } - else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = (vnode.listener = - oldVnode.listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - elm.addEventListener(name, listener, false); - } - } - else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } - } - const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners - }; - const xlinkNS = "http://www.w3.org/1999/xlink"; - const xmlNS = "http://www.w3.org/XML/1998/namespace"; - const colonChar = 58; - const xChar = 120; - function updateAttrs(oldVnode, vnode) { - var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) - return; - if (oldAttrs === attrs) - return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } - else if (cur === false) { - elm.removeAttribute(key); - } - else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } - else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } - else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } - else { - elm.setAttribute(key, cur); - } - } - } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } - } - const attrsModule = { - create: updateAttrs, - update: updateAttrs - }; - const patch$1 = init([eventListenersModule, attrsModule, propsModule]); - - let getId = idGenerator(); - //------------------------------------------------------------------------------ - // Widget - //------------------------------------------------------------------------------ - class Component extends EventBus { - //-------------------------------------------------------------------------- - // Lifecycle - //-------------------------------------------------------------------------- - /** - * Creates an instance of Component. - * - * The root widget of a component tree needs an environment: - * - * ```javascript - * const root = new RootWidget(env, props); - * ``` - * - * Every other widget simply needs a reference to its parent: - * - * ```javascript - * const child = new SomeWidget(parent, props); - * ``` - * - * Note that most of the time, only the root widget needs to be created by - * hand. Other widgets should be created automatically by the framework (with - * the t-widget directive in a template) - */ - constructor(parent, props) { - super(); - this.template = "default"; - this.inlineTemplate = null; - this.refs = {}; - // is this a good idea? - // Pro: if props is empty, we can create easily a widget - // Con: this is not really safe - // Pro: but creating widget (by a template) is always unsafe anyway - this.props = props || {}; - let id = getId(); - let p = null; - if (parent instanceof Component) { - p = parent; - this.env = parent.env; - parent.__owl__.children[id] = this; - } - else { - this.env = parent; - } - this.__owl__ = { - id: id, - vnode: null, - isStarted: false, - isMounted: false, - isDestroyed: false, - parent: p, - children: {}, - cmap: {}, - renderId: 1, - renderPromise: null, - renderProps: props || null, - boundHandlers: {} - }; - } - get el() { - return this.__owl__.vnode ? this.__owl__.vnode.elm : null; - } - /** - * willStart is an asynchronous hook that can be implemented to perform some - * action before the initial rendering of a component. - * - * It will be called exactly once before the initial rendering. It is useful - * in some cases, for example, to load external assets (such as a JS library) - * before the widget is rendered. - * - * Note that a slow willStart method will slow down the rendering of the user - * interface. Therefore, some effort should be made to make this method as - * fast as possible. - * - * Note: this method should not be called manually. - */ - async willStart() { } - /** - * mounted is a hook that is called each time a component is attached to the - * DOM. This is a good place to add some listeners, or to interact with the - * DOM, if the component needs to perform some measure for example. - * - * Note: this method should not be called manually. - * - * @see willUnmount - */ - mounted() { } - /** - * The willUpdateProps is an asynchronous hook, called just before new props - * are set. This is useful if the component needs some asynchronous task - * performed, depending on the props (for example, assuming that the props are - * some record Id, fetching the record data). - * - * This hook is not called during the first render (but willStart is called - * and performs a similar job). - */ - async willUpdateProps(nextProps) { } - /** - * The willPatch hook is called just before the DOM patching process starts. - * It is not called on the initial render. This is useful to get some - * information which are in the DOM. For example, the current position of the - * scrollbar - */ - willPatch() { } - /** - * This hook is called whenever a component did actually update its props, - * state or env. - * - * This method is not called on the initial render. It is useful to interact - * with the DOM (for example, through an external library) whenever the - * component was updated. - * - * Updating the widget state in this hook is possible, but not encouraged. - * One need to be careful, because updates here will cause rerender, which in - * turn will cause other calls to updated. So, we need to be particularly - * careful at avoiding endless cycles. - */ - patched() { } - /** - * willUnmount is a hook that is called each time just before a component is - * unmounted from the DOM. This is a good place to remove some listeners, for - * example. - * - * Note: this method should not be called manually. - * - * @see mounted - */ - willUnmount() { } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - /** - * Attach a child widget to a given html element - * - * This is most of the time not necessary, since widgets should primarily be - * created/managed with the t-widget directive in a qweb template. However, - * for the cases where we need more control, this method will do what is - * necessary to make sure all the proper hooks are called (for example, - * mounted/willUnmount) - * - * Note that this method makes a few assumptions: - * - the child widget is indeed a child of the current widget - * - the target is inside the dom of the current widget (typically a ref) - */ - attachChild(child, target) { - target.appendChild(child.el); - child.__mount(); - } - async mount(target) { - const vnode = await this._prepare(); - if (this.__owl__.isDestroyed) { - // widget was destroyed before we get here... - return; - } - this._patch(vnode); - target.appendChild(this.el); - if (document.body.contains(target)) { - this._visitSubTree(w => { - if (!w.__owl__.isMounted && this.el.contains(w.el)) { - w.__owl__.isMounted = true; - w.mounted(); - return true; - } - return false; - }); - } - } - unmount() { - if (this.el) { - this._visitSubTree(w => { - if (w.__owl__.isMounted) { - w.willUnmount(); - w.__owl__.isMounted = false; - return true; - } - return false; - }); - this.el.remove(); - } - } - async render(force = false) { - if (this.__owl__.isDestroyed) { - return; - } - const renderVDom = this._render(force); - const renderId = this.__owl__.renderId; - const vnode = await renderVDom; - if (renderId === this.__owl__.renderId) { - // we only update the vnode and the actual DOM if no other rendering - // occurred between now and when the render method was initially called. - this._patch(vnode); - } - } - destroy() { - if (!this.__owl__.isDestroyed) { - for (let id in this.__owl__.children) { - this.__owl__.children[id].destroy(); - } - if (this.__owl__.isMounted) { - this.willUnmount(); - } - if (this.el) { - this.el.remove(); - this.__owl__.isMounted = false; - delete this.__owl__.vnode; - } - if (this.__owl__.parent) { - let id = this.__owl__.id; - delete this.__owl__.parent.__owl__.children[id]; - this.__owl__.parent = null; - } - this.clear(); - this.__owl__.isDestroyed = true; - } - } - shouldUpdate(nextProps) { - return true; - } - /** - * This method is the correct way to update the environment of a widget. Doing - * this will cause a full rerender of the widget and its children, so this is - * an operation that should not be done frequently. - * - * A good usecase for updating the environment would be to update some mostly - * static config keys, such as a boolean to determine if we are in mobile - * mode or not. - */ - async updateEnv(nextEnv) { - if (this.__owl__.parent && this.__owl__.parent.env === this.env) { - this.env = Object.create(this.env); - } - Object.assign(this.env, nextEnv); - if (this.__owl__.isMounted) { - await this.render(true); - } - } - async updateProps(nextProps, forceUpdate = false) { - const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); - return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve(); - } - set(target, key, value) { - this.__owl__.observer.set(target, key, value); - } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - async _updateProps(nextProps) { - await this.willUpdateProps(nextProps); - this.props = nextProps; - await this.render(); - } - _patch(vnode) { - this.__owl__.renderPromise = null; - if (this.__owl__.vnode) { - this.willPatch(); - this.__owl__.vnode = patch$1(this.__owl__.vnode, vnode); - this.patched(); - } - else { - this.__owl__.vnode = patch$1(document.createElement(vnode.sel), vnode); - } - } - async _prepare() { - this.__owl__.renderProps = this.props; - this.__owl__.renderPromise = this.willStart().then(() => { - if (this.__owl__.isDestroyed) { - return Promise.resolve(h("div")); - } - this.__owl__.isStarted = true; - if (this.inlineTemplate) { - this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true); - } - this._observeState(); - return this._render(); - }); - return this.__owl__.renderPromise; - } - async _render(force = false) { - this.__owl__.renderId++; - const promises = []; - const template = this.inlineTemplate || this.template; - if (this.__owl__.observer) { - this.__owl__.observer.allowMutations = false; - } - let vnode = this.env.qweb.render(template, this, { - promises, - handlers: this.__owl__.boundHandlers, - forceUpdate: force - }); - if (this.__owl__.observer) { - this.__owl__.observer.allowMutations = true; - } - // this part is critical for the patching process to be done correctly. The - // tricky part is that a child widget can be rerendered on its own, which - // will update its own vnode representation without the knowledge of the - // parent widget. With this, we make sure that the parent widget will be - // able to patch itself properly after - vnode.key = this.__owl__.id; - this.__owl__.renderProps = this.props; - this.__owl__.renderPromise = Promise.all(promises).then(() => vnode); - return this.__owl__.renderPromise; - } - /** - * Only called by qweb t-widget directive - */ - _mount(vnode, elm) { - this.__owl__.vnode = patch$1(elm, vnode); - this.__mount(); - return this.__owl__.vnode; - } - __mount() { - if (this.__owl__.isMounted) { - return; - } - this._observeState(); - if (this.__owl__.parent) { - if (this.__owl__.parent.__owl__.isMounted) { - this.__owl__.isMounted = true; - this.mounted(); - const children = this.__owl__.children; - for (let id in children) { - children[id].__mount(); - } - } - } - } - _visitSubTree(callback) { - const shouldVisitChildren = callback(this); - if (shouldVisitChildren) { - const children = this.__owl__.children; - for (let id in children) { - children[id]._visitSubTree(callback); - } - } - } - _observeState() { - if (this.state) { - this.__owl__.observer = new Observer(); - this.__owl__.observer.observe(this.state); - this.__owl__.observer.notifyCB = this.render.bind(this); - } - } - } - - const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(","); - const WORD_REPLACEMENT = { - and: "&&", - or: "||", - gt: ">", - gte: ">=", - lt: "<", - lte: "<=" - }; - const DISABLED_TAGS = [ - "input", - "textarea", - "button", - "select", - "option", - "optgroup" - ]; - const lineBreakRE = /[\r\n]/; - const whitespaceRE = /\s+/g; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class Context { - constructor(name) { - this.nextID = 1; - this.code = []; - this.variables = {}; - this.definedVariables = {}; - this.escaping = false; - this.parentNode = null; - this.rootNode = null; - this.indentLevel = 0; - this.shouldDefineOwner = false; - this.shouldProtectContext = false; - this.inLoop = false; - this.inPreTag = false; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("var h = this.utils.h;"); - } - generateID() { - const id = this.rootContext.nextID++; - return id; - } - withParent(node) { - if (this === this.rootContext && this.parentNode) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - return this.subContext("parentNode", node); - } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.indentLevel++; - } - dedent() { - this.indentLevel--; - } - addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - closeIf() { - this.dedent(); - this.addLine("}"); - } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - formatExpression(e) { - e = e.trim(); - if (e[0] === "{" && e[e.length - 1] === "}") { - const innerExpr = e - .slice(1, -1) - .split(",") - .map(p => { - let [key, val] = p.trim().split(":"); - if (key === "") { - return ""; - } - if (!val) { - val = key; - } - return `${key}: ${this.formatExpression(val)}`; - }) - .join(","); - return "{" + innerExpr + "}"; - } - // Thanks CHM for this code... - const chars = e.split(""); - let instring = ""; - let invar = ""; - let invarPos = 0; - let r = ""; - chars.push(" "); - for (var i = 0, ilen = chars.length; i < ilen; i++) { - var c = chars[i]; - if (instring.length) { - if (c === instring && chars[i - 1] !== "\\") { - instring = ""; - } - } - else if (c === '"' || c === "'") { - instring = c; - } - else if (c.match(/[a-zA-Z_\$]/) && !invar.length) { - invar = c; - invarPos = i; - continue; - } - else if (c.match(/\W/) && invar.length) { - // TODO: Should check for possible spaces before dot - if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) { - if (!(invar in this.definedVariables)) { - invar = - WORD_REPLACEMENT[invar] || - (invar in this.variables && - this.formatExpression(this.variables[invar])) || - "context['" + invar + "']"; - } - } - r += invar; - invar = ""; - } - else if (invar.length) { - invar += c; - continue; - } - r += c; - } - const result = r.slice(0, -1); - return result; - } - } - //------------------------------------------------------------------------------ - // QWeb rendering engine - //------------------------------------------------------------------------------ - class QWeb { - constructor(data) { - this.processedTemplates = {}; - this.templates = {}; - this.directives = []; - this.utils = { - h: h, - getFragment(str) { - const temp = document.createElement("template"); - temp.innerHTML = str; - return temp.content; - }, - objectToAttrString(obj) { - let classes = []; - for (let k in obj) { - if (obj[k]) { - classes.push(k); - } - } - return classes.join(" "); - } - }; - this.directiveNames = { - as: 1, - name: 1, - value: 1, - att: 1, - attf: 1, - props: 1, - key: 1, - keepalive: 1, - debug: 1 - }; - [ - forEachDirective, - escDirective, - rawDirective, - setDirective, - elseDirective, - elifDirective, - ifDirective, - callDirective, - onDirective, - refDirective, - widgetDirective - ].forEach(d => this.addDirective(d)); - if (data) { - this.loadTemplates(data); - } - } - addDirective(dir) { - this.directives.push(dir); - this.directiveNames[dir.name] = 1; - this.directives.sort((d1, d2) => d1.priority - d2.priority); - } - /** - * Add a template to the internal template map. Note that it is not - * immediately compiled. - */ - addTemplate(name, template, allowDuplicates = false) { - if (name in this.processedTemplates) { - if (allowDuplicates) { - return; - } - else { - throw new Error(`Template ${name} already defined`); - } - } - const parser = new DOMParser(); - const doc = parser.parseFromString(template, "text/xml"); - if (!doc.firstChild) { - throw new Error("Invalid template (should not be empty)"); - } - if (doc.getElementsByTagName("parsererror").length) { - throw new Error("Invalid XML in template"); - } - let elem = doc.firstChild; - this._processTemplate(elem); - this.processedTemplates[name] = elem; - } - _processTemplate(elem) { - let tbranch = elem.querySelectorAll("[t-elif], [t-else]"); - for (let i = 0, ilen = tbranch.length; i < ilen; i++) { - let node = tbranch[i]; - let prevElem = node.previousElementSibling; - let pattr = function (name) { - return prevElem.getAttribute(name); - }; - let nattr = function (name) { - return +!!node.getAttribute(name); - }; - if (prevElem && (pattr("t-if") || pattr("t-elif"))) { - if (pattr("t-foreach")) { - throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else"); - } - if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) { - return a + b; - }) > 1) { - throw new Error("Only one conditional branching directive is allowed per node"); - } - // All text nodes between branch nodes are removed - let textNode; - while ((textNode = node.previousSibling) !== prevElem) { - if (textNode.nodeValue.trim().length) { - throw new Error("text is not allowed between branching directives"); - } - textNode.remove(); - } - } - else { - throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive"); - } - } - } - /** - * Load templates from a xml (as a string). This will look up for the first - * tag, and will consider each child of this as a template, with - * the name given by the t-name attribute. - */ - loadTemplates(xmlstr) { - const parser = new DOMParser(); - const doc = parser.parseFromString(xmlstr, "text/xml"); - if (doc.getElementsByTagName("parsererror").length) { - throw new Error("Invalid XML in template"); - } - const templates = doc.getElementsByTagName("templates")[0]; - if (!templates) { - return; - } - for (let elem of templates.children) { - const name = elem.getAttribute("t-name"); - this._processTemplate(elem); - this.processedTemplates[name] = elem; - } - } - /** - * Render a template - * - * @param {string} name the template should already have been added - */ - render(name, context = {}, extra = null) { - if (!(name in this.processedTemplates)) { - throw new Error(`Template ${name} does not exist`); - } - const template = this.templates[name] || this._compile(name); - return template.call(this, context, extra); - } - _compile(name) { - if (name in this.templates) { - return this.templates[name]; - } - const mainNode = this.processedTemplates[name]; - const isDebug = mainNode.attributes.hasOwnProperty("t-debug"); - const ctx = new Context(name); - this._compileNode(mainNode, ctx); - if (ctx.shouldProtectContext) { - ctx.code.unshift(" context = Object.create(context);"); - } - if (ctx.shouldDefineOwner) { - // this is necessary to prevent some directives (t-forach for ex) to - // pollute the rendering context by adding some keys in it. - ctx.code.unshift(" let owner = context;"); - } - if (!ctx.rootNode) { - throw new Error("A template should have one root node"); - } - ctx.addLine(`return vn${ctx.rootNode};`); - if (isDebug) { - ctx.code.unshift(" debugger"); - } - let template; - try { - template = new Function("context", "extra", ctx.code.join("\n")); - } - catch (e) { - throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`); - } - if (isDebug) { - console.log(`Template: ${this.processedTemplates[name].outerHTML}\nCompiled code:\n` + template.toString()); - } - this.templates[name] = template; - return template; - } - /** - * Generate code from an xml node - * - */ - _compileNode(node, ctx) { - if (!(node instanceof Element)) { - // this is a text node, there are no directive to apply - let text = node.textContent; - if (!ctx.inPreTag) { - if (lineBreakRE.test(text) && !text.trim()) { - return; - } - text = text.replace(whitespaceRE, " "); - } - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); - } - else { - // this is an unusual situation: this text node is the result of the - // template rendering. - let nodeID = ctx.generateID(); - ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); - ctx.rootContext.rootNode = nodeID; - ctx.rootContext.parentNode = nodeID; - } - return; - } - const attributes = node.attributes; - const validDirectives = []; - let withHandlers = false; - // maybe this is not optimal: we iterate on all attributes here, and again - // just after for each directive. - for (let i = 0; i < attributes.length; i++) { - let attrName = attributes[i].name; - if (attrName.startsWith("t-")) { - let dName = attrName.slice(2).split("-")[0]; - if (!(dName in this.directiveNames)) { - throw new Error(`Unknown QWeb directive: '${attrName}'`); - } - } - } - for (let directive of this.directives) { - let fullName; - let value; - for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; - if (name === "t-" + directive.name || - name.startsWith("t-" + directive.name + "-")) { - fullName = name; - value = attributes[i].textContent; - validDirectives.push({ directive, value, fullName }); - if (directive.name === "on") { - withHandlers = true; - } - } - } - } - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeEncounter) { - const isDone = directive.atNodeEncounter({ - node, - qweb: this, - ctx, - fullName, - value - }); - if (isDone) { - return; - } - } - } - if (node.nodeName !== "t") { - let nodeID = this._compileGenericNode(node, ctx, withHandlers); - ctx = ctx.withParent(nodeID); - for (let { directive, value, fullName } of validDirectives) { - if (directive.atNodeCreation) { - directive.atNodeCreation({ - node, - qweb: this, - ctx, - fullName, - value, - nodeID - }); - } - } - } - if (node.nodeName === "pre") { - ctx = ctx.subContext("inPreTag", true); - } - this._compileChildren(node, ctx); - for (let { directive, value, fullName } of validDirectives) { - if (directive.finalize) { - directive.finalize({ node, qweb: this, ctx, fullName, value }); - } - } - } - _compileGenericNode(node, ctx, withHandlers = true) { - // nodeType 1 is generic tag - if (node.nodeType !== 1) { - throw new Error("unsupported node type"); - } - const attributes = node.attributes; - const attrs = []; - const props = []; - const tattrs = []; - function handleBooleanProps(key, val) { - let isProp = false; - if (node.nodeName === "input" && key === "checked") { - let type = node.getAttribute("type"); - if (type === "checkbox" || type === "radio") { - isProp = true; - } - } - if (node.nodeName === "option" && key === "selected") { - isProp = true; - } - if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { - isProp = true; - } - if ((key === "readonly" && node.nodeName === "input") || - node.nodeName === "textarea") { - isProp = true; - } - if (isProp) { - props.push(`${key}: _${val}`); - } - } - for (let i = 0; i < attributes.length; i++) { - let name = attributes[i].name; - const value = attributes[i].textContent; - // regular attributes - if (!name.startsWith("t-") && - !node.getAttribute("t-attf-" + name)) { - const attID = ctx.generateID(); - ctx.addLine(`var _${attID} = '${value}';`); - if (!name.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - name = '"' + name + '"'; - } - attrs.push(`${name}: _${attID}`); - handleBooleanProps(name, attID); - } - // dynamic attributes - if (name.startsWith("t-att-")) { - let attName = name.slice(6); - let formattedValue = ctx.formatExpression(ctx.getValue(value)); - if (formattedValue[0] === "{" && - formattedValue[formattedValue.length - 1] === "}") { - formattedValue = `this.utils.objectToAttrString(${formattedValue})`; - } - const attID = ctx.generateID(); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - // we need to combine dynamic with non dynamic attributes: - // class="a" t-att-class="'yop'" should be rendered as class="a yop" - const attValue = node.getAttribute(attName); - if (attValue) { - const attValueID = ctx.generateID(); - ctx.addLine(`var _${attValueID} = ${formattedValue};`); - formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; - const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); - attrs.splice(attrIndex, 1); - } - ctx.addLine(`var _${attID} = ${formattedValue};`); - attrs.push(`${attName}: _${attID}`); - handleBooleanProps(attName, attID); - } - if (name.startsWith("t-attf-")) { - let attName = name.slice(7); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; - } - const formattedExpr = value.replace(/\{\{.*?\}\}/g, s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"); - const attID = ctx.generateID(); - let staticVal = node.getAttribute(attName); - if (staticVal) { - ctx.addLine(`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`); - } - else { - ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`); - } - attrs.push(`${attName}: _${attID}`); - } - // t-att= attributes - if (name === "t-att") { - let id = ctx.generateID(); - ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`); - tattrs.push(id); - } - } - let nodeID = ctx.generateID(); - let nodeKey = node.getAttribute("t-key"); - if (nodeKey) { - nodeKey = ctx.formatExpression(nodeKey); - } - else { - nodeKey = nodeID; - } - const parts = [`key:${nodeKey}`]; - if (attrs.length + tattrs.length > 0) { - parts.push(`attrs:{${attrs.join(",")}}`); - } - if (props.length > 0) { - parts.push(`props:{${props.join(",")}}`); - } - if (withHandlers) { - parts.push(`on:{}`); - } - ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`); - for (let id of tattrs) { - ctx.addIf(`_${id} instanceof Array`); - ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`); - ctx.addElse(); - ctx.addLine(`for (let key in _${id}) {`); - ctx.indent(); - ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`); - ctx.dedent(); - ctx.addLine(`}`); - ctx.closeIf(); - } - ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); - } - return nodeID; - } - _compileChildren(node, ctx) { - if (node.childNodes.length > 0) { - for (let child of Array.from(node.childNodes)) { - this._compileNode(child, ctx); - } - } - } - } - function compileValueNode(value, node, qweb, ctx) { - if (value === "0" && ctx.caller) { - qweb._compileNode(ctx.caller, ctx); - return; - } - if (typeof value === "string") { - let exprID = value; - if (!(value in ctx.definedVariables)) { - exprID = `_${ctx.generateID()}`; - ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`); - } - ctx.addIf(`${exprID} || ${exprID} === 0`); - if (!ctx.parentNode) { - throw new Error("Should not have a text node without a parent"); - } - if (ctx.escaping) { - ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`); - } - else { - let fragID = ctx.generateID(); - ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); - let tempNodeID = ctx.generateID(); - ctx.addLine(`var p${tempNodeID} = {hook: {`); - ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); - ctx.addLine(`}};`); - ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); - ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); - } - if (node.childNodes.length) { - ctx.addElse(); - qweb._compileChildren(node, ctx); - } - ctx.closeIf(); - return; - } - if (value instanceof NodeList) { - for (let node of Array.from(value)) { - qweb._compileNode(node, ctx); - } - } - } - const escDirective = { - name: "esc", - priority: 70, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-esc")); - compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); - return true; - } - }; - const rawDirective = { - name: "raw", - priority: 80, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } - let value = ctx.getValue(node.getAttribute("t-raw")); - compileValueNode(value, node, qweb, ctx); - return true; - } - }; - const setDirective = { - name: "set", - priority: 60, - atNodeEncounter({ node, ctx }) { - const variable = node.getAttribute("t-set"); - let value = node.getAttribute("t-value"); - if (value) { - const formattedValue = ctx.formatExpression(value); - if (ctx.variables.hasOwnProperty(variable)) { - ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`); - } - else { - const varName = `_${ctx.generateID()}`; - ctx.addLine(`var ${varName} = ${formattedValue};`); - ctx.definedVariables[varName] = formattedValue; - ctx.variables[variable] = varName; - } - } - else { - ctx.variables[variable] = node.childNodes; - } - return true; - } - }; - const ifDirective = { - name: "if", - priority: 20, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-if")); - ctx.addIf(`${ctx.formatExpression(cond)}`); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }; - const elifDirective = { - name: "elif", - priority: 30, - atNodeEncounter({ node, ctx }) { - let cond = ctx.getValue(node.getAttribute("t-elif")); - ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }; - const elseDirective = { - name: "else", - priority: 40, - atNodeEncounter({ ctx }) { - ctx.addLine(`else {`); - ctx.indent(); - return false; - }, - finalize({ ctx }) { - ctx.closeIf(); - } - }; - const callDirective = { - name: "call", - priority: 50, - atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - throw new Error("Invalid tag for t-call directive (should be 't')"); - } - const subTemplate = node.getAttribute("t-call"); - const nodeTemplate = qweb.processedTemplates[subTemplate]; - if (!nodeTemplate) { - throw new Error(`Cannot find template "${subTemplate}" (t-call)`); - } - const nodeCopy = node.cloneNode(true); - nodeCopy.removeAttribute("t-call"); - // extract variables from nodecopy - const tempCtx = new Context(); - tempCtx.nextID = ctx.rootContext.nextID; - qweb._compileNode(nodeCopy, tempCtx); - const vars = Object.assign({}, ctx.variables, tempCtx.variables); - var definedVariables = Object.assign({}, ctx.definedVariables, tempCtx.definedVariables); - ctx.rootContext.nextID = tempCtx.nextID; - // open new scope, if necessary - const hasNewVariables = Object.keys(definedVariables).length > 0; - if (hasNewVariables) { - ctx.addLine("{"); - ctx.indent(); - } - // add new variables, if any - for (let key in definedVariables) { - ctx.addLine(`let ${key} = ${definedVariables[key]}`); - } - // compile sub template - const subCtx = ctx - .subContext("caller", nodeCopy) - .subContext("variables", Object.create(vars)) - .subContext("definedVariables", Object.create(definedVariables)); - qweb._compileNode(nodeTemplate, subCtx); - // close new scope - if (hasNewVariables) { - ctx.dedent(); - ctx.addLine("}"); - } - return true; - } - }; - const forEachDirective = { - name: "foreach", - priority: 10, - atNodeEncounter({ node, qweb, ctx }) { - ctx.rootContext.shouldProtectContext = true; - ctx = ctx.subContext("inLoop", true); - const elems = node.getAttribute("t-foreach"); - const name = node.getAttribute("t-as"); - let arrayID = ctx.generateID(); - ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); - ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); - ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`); - let keysID = ctx.generateID(); - ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`); - let valuesID = ctx.generateID(); - ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`); - ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`); - ctx.indent(); - ctx.addLine(`context.${name}_first = i === 0;`); - ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`); - ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`); - ctx.addLine(`context.${name}_index = i;`); - ctx.addLine(`context.${name} = _${keysID}[i];`); - ctx.addLine(`context.${name}_value = _${valuesID}[i];`); - const nodeCopy = node.cloneNode(true); - if (nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key")) { - console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`); - } - nodeCopy.removeAttribute("t-foreach"); - qweb._compileNode(nodeCopy, ctx); - ctx.dedent(); - ctx.addLine("}"); - return true; - } - }; - const onDirective = { - name: "on", - priority: 90, - atNodeCreation({ ctx, fullName, value, nodeID }) { - ctx.rootContext.shouldDefineOwner = true; - const eventName = fullName.slice(5); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handler = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`; - if (extraArgs) { - ctx.addLine(`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`); - } - else { - ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } - } - }; - const refDirective = { - name: "ref", - priority: 95, - atNodeCreation({ ctx, node }) { - let ref = node.getAttribute("t-ref"); - ctx.addLine(`p${ctx.parentNode}.hook = { - create: (_, n) => context.refs[${ctx.formatExpression(ref)}] = n.elm, - };`); - } - }; - const widgetDirective = { - name: "widget", - priority: 100, - atNodeEncounter({ ctx, value, node }) { - ctx.addLine("//WIDGET"); - ctx.rootContext.shouldDefineOwner = true; - let props = node.getAttribute("t-props"); - let keepAlive = node.getAttribute("t-keepalive") ? true : false; - // t-on- events... - const events = []; - const attributes = node.attributes; - for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; - if (name.startsWith("t-on-")) { - events.push([name.slice(5), attributes[i].textContent]); - } - } - let key = node.getAttribute("t-key"); - if (key) { - key = ctx.formatExpression(key); - } - if (props) { - props = ctx.formatExpression(props); - } - let dummyID = ctx.generateID(); - let defID = ctx.generateID(); - let widgetID = ctx.generateID(); - let keyID = key && ctx.generateID(); - if (key) { - // we bind a variable to the key (could be a complex expression, so we - // want to evaluate it only once) - ctx.addLine(`let key${keyID} = ${key};`); - } - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); - ctx.addLine(`c${ctx.parentNode}.push(null);`); - ctx.addLine(`let def${defID};`); - let templateID = key - ? `key${keyID}` - : ctx.inLoop - ? `String(-${widgetID} - i)` - : String(widgetID); - ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); - ctx.addLine(`let props${widgetID} = ${props || "{}"};`); - ctx.addLine(`let isNew${widgetID} = !w${widgetID};`); - // check if we can reuse current rendering promise - ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`); - ctx.addIf(`w${widgetID}.__owl__.isStarted`); - ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`); - ctx.addElse(); - ctx.addLine(`isNew${widgetID} = true`); - ctx.addIf(`props${widgetID} === w${widgetID}.__owl__.renderProps`); - ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`); - ctx.addElse(); - ctx.addLine(`w${widgetID}.destroy();`); - ctx.addLine(`w${widgetID} = false`); - ctx.closeIf(); - ctx.closeIf(); - ctx.closeIf(); - ctx.addIf(`!def${defID}`); - ctx.addIf(`w${widgetID}`); - ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`); - ctx.addElse(); - ctx.addLine(`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`); - ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`); - for (let [event, method] of events) { - ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`); - } - let ref = node.getAttribute("t-ref"); - let refExpr = ref ? `context.refs[${ctx.formatExpression(ref)}] = w${widgetID};` : ''; - ctx.addLine(`def${defID} = w${widgetID}._prepare();`); - ctx.closeIf(); - ctx.closeIf(); - ctx.addIf(`isNew${widgetID}`); - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){w${widgetID}.${keepAlive ? "unmount" : "destroy"}()},destroy(){w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}}; w${widgetID}.__owl__.pvnode = pvnode;});`); - ctx.addElse(); - ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}, destroy() {w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}}}c${ctx.parentNode}[_${dummyID}_index]=vnode;});`); - ctx.closeIf(); - ctx.addLine(`extra.promises.push(def${defID});`); - if (node.hasAttribute("t-if") || node.hasAttribute("t-else")) { - ctx.closeIf(); - } - return true; - } - }; - - class Store extends EventBus { - constructor(config, options = {}) { - super(); - this._commitLevel = 0; - this.history = []; - this.debug = options.debug || false; - this.state = config.state || {}; - this.actions = config.actions; - this.mutations = config.mutations; - this.env = config.env; - this.observer = new Observer(); - this.observer.notifyCB = this.trigger.bind(this, "update"); - this.observer.allowMutations = false; - this.observer.observe(this.state); - if (this.debug) { - this.history.push({ state: this.state }); - } - this.set = this.observer.set.bind(this.observer); - } - dispatch(action, payload) { - if (!this.actions[action]) { - throw new Error(`[Error] action ${action} is undefined`); - } - const result = this.actions[action]({ - commit: this.commit.bind(this), - dispatch: this.dispatch.bind(this), - env: this.env, - state: this.state - }, payload); - if (result instanceof Promise) { - return new Promise((resolve, reject) => { - result.then(() => resolve()); - result.catch(reject); - }); - } - } - commit(type, payload) { - if (!this.mutations[type]) { - throw new Error(`[Error] mutation ${type} is undefined`); - } - this._commitLevel++; - this.observer.allowMutations = true; - const res = this.mutations[type].call(null, { - commit: this.commit.bind(this), - state: this.state, - set: this.set - }, payload); - if (this._commitLevel === 1) { - this.observer.allowMutations = false; - if (this.debug) { - this.history.push({ - state: this.state, - mutation: type, - payload: payload - }); - } - } - this._commitLevel--; - return res; - } - } - //------------------------------------------------------------------------------ - // Connect function - //------------------------------------------------------------------------------ - function revNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.rev; - } - return 0; - } - function deepRevNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.deepRev; - } - return 0; - } - function connect(mapStateToProps, options = {}) { - let hashFunction = options.hashFunction || null; - if (!hashFunction) { - let deep = "deep" in options ? options.deep : true; - let defaultRevFunction = deep ? deepRevNumber : revNumber; - hashFunction = function ({ storeProps }, options) { - const { currentStoreProps } = options; - if ("__owl__" in storeProps) { - return defaultRevFunction(storeProps); - } - let hash = 0; - for (let key in storeProps) { - const val = storeProps[key]; - const hashVal = defaultRevFunction(val); - if (hashVal === 0) { - if (val !== currentStoreProps[key]) { - options.didChange = true; - } - } - else { - hash += hashVal; - } - } - return hash; - }; - } - return function (Comp) { - return class extends Comp { - constructor(parent, props) { - const env = parent instanceof Component ? parent.env : parent; - const ownProps = Object.assign({}, props || {}); - const storeProps = mapStateToProps(env.store.state, ownProps); - const mergedProps = Object.assign({}, props || {}, storeProps); - super(parent, mergedProps); - this.__owl__.ownProps = ownProps; - this.__owl__.currentStoreProps = storeProps; - this.__owl__.storeHash = hashFunction({ - state: env.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, { - currentStoreProps: storeProps - }); - } - mounted() { - this.env.store.on("update", this, () => { - const ownProps = this.__owl__.ownProps; - const storeProps = mapStateToProps(this.env.store.state, ownProps); - const options = { - currentStoreProps: this.__owl__.currentStoreProps - }; - const storeHash = hashFunction({ - state: this.env.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, options); - let didChange = options.didChange; - if (storeHash !== this.__owl__.storeHash) { - didChange = true; - this.__owl__.storeHash = storeHash; - } - if (didChange) { - this.__owl__.currentStoreProps = storeProps; - this.updateProps(ownProps, false); - } - }); - super.mounted(); - } - willUnmount() { - this.env.store.off("update", this); - super.willUnmount(); - } - updateProps(nextProps, forceUpdate) { - if (this.__owl__.ownProps !== nextProps) { - this.__owl__.currentStoreProps = mapStateToProps(this.env.store.state, nextProps); - } - this.__owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps); - return super.updateProps(mergedProps, forceUpdate); - } - }; - }; - } - - const utils = _utils; - - exports.utils = utils; - exports.Component = Component; - exports.EventBus = EventBus; - exports.QWeb = QWeb; - exports.connect = connect; - exports.Store = Store; - - exports._version = '0.8.0'; - exports._date = '2019-04-24T08:12:21.164Z'; - exports._hash = 'b14d264'; - exports._url = 'https://github.com/odoo/owl'; - -}(this.owl = this.owl || {})); diff --git a/tools/benchmarks/owl-0.8.0/templates.xml b/tools/benchmarks/owl-0.8.0/templates.xml deleted file mode 100644 index 699ebea3..00000000 --- a/tools/benchmarks/owl-0.8.0/templates.xml +++ /dev/null @@ -1,50 +0,0 @@ - -
-
-
Actions
-
- - - - - - -
-
-
- - -
-
- - -
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
- - - -
-
-
- -
- - - - -
- -
- -
- - \ No newline at end of file diff --git a/tools/benchmarks/owl-0.14.0/app.js b/tools/benchmarks/owl-1.0.0-beta1/app.js similarity index 83% rename from tools/benchmarks/owl-0.14.0/app.js rename to tools/benchmarks/owl-1.0.0-beta1/app.js index e739aeb1..caa88b4b 100644 --- a/tools/benchmarks/owl-0.14.0/app.js +++ b/tools/benchmarks/owl-1.0.0-beta1/app.js @@ -1,10 +1,11 @@ import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js"; +const { useState, useRef } = owl.hooks; //------------------------------------------------------------------------------ // Likes Counter Widget //------------------------------------------------------------------------------ class Counter extends owl.Component { - state = { counter: 0 }; + state = useState({ counter: 0 }); increment() { this.state.counter++; @@ -15,8 +16,6 @@ class Counter extends owl.Component { // Message Widget //------------------------------------------------------------------------------ class Message extends owl.Component { - widgets = { Counter }; - shouldUpdate(nextProps) { return nextProps.message !== this.props.message; } @@ -26,13 +25,14 @@ class Message extends owl.Component { }); } } +Message.components = { Counter }; //------------------------------------------------------------------------------ // Root Widget //------------------------------------------------------------------------------ class App extends owl.Component { - widgets = { Message }; - state = { messages: [], multipleFlag: false, clearAfterFlag: false }; + state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false }); + logRef = useRef("log"); mounted() { this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); @@ -108,10 +108,10 @@ class App extends owl.Component { updateSomeMessages() { this.benchmark("update every 10th", () => { const messages = this.state.messages; - for (let i = 0; i < this.state.messages.length; i += 10) { + for (let i = 0; i < messages.length; i += 10) { const msg = Object.assign({}, messages[i]); msg.author += "!!!"; - this.set(messages, i, msg); + messages[i] = msg; } }); } @@ -129,32 +129,25 @@ class App extends owl.Component { div.classList.add("bold"); } div.textContent = `> ${str}`; - this.refs.log.appendChild(div); - this.refs.log.scrollTop = this.refs.log.scrollHeight; + this.logRef.el.appendChild(div); + this.logRef.el.scrollTop = this.logRef.el.scrollHeight; } clearLog() { - this.refs.log.innerHTML = ""; - } - - toggleMultiple() { - this.state.multipleFlag = !this.state.multipleFlag; - } - - toggleClear() { - this.state.clearAfterFlag = !this.state.clearAfterFlag; + this.logRef.el.innerHTML = ""; } } +App.components = { Message }; //------------------------------------------------------------------------------ // Application initialization //------------------------------------------------------------------------------ async function start() { - const templates = await owl.utils.loadTemplates("templates.xml"); - const env = { - qweb: new owl.QWeb(templates) + const templates = await owl.utils.loadFile("templates.xml"); + App.env = { + qweb: new owl.QWeb({ templates }) }; - const app = new App(env); + const app = new App(); app.mount(document.body); } diff --git a/tools/benchmarks/owl-0.12.0/index.html b/tools/benchmarks/owl-1.0.0-beta1/index.html similarity index 78% rename from tools/benchmarks/owl-0.12.0/index.html rename to tools/benchmarks/owl-1.0.0-beta1/index.html index 66f1950e..13aa45ec 100644 --- a/tools/benchmarks/owl-0.12.0/index.html +++ b/tools/benchmarks/owl-1.0.0-beta1/index.html @@ -2,12 +2,11 @@ - OWL 0.12.0 Benchmark + OWL v1.0.0-beta1 Benchmark -
diff --git a/tools/benchmarks/owl-0.16.0/owl.js b/tools/benchmarks/owl-1.0.0-beta1/owl.js similarity index 54% rename from tools/benchmarks/owl-0.16.0/owl.js rename to tools/benchmarks/owl-1.0.0-beta1/owl.js index fd8d41ed..dfbc49f5 100644 --- a/tools/benchmarks/owl-0.16.0/owl.js +++ b/tools/benchmarks/owl-1.0.0-beta1/owl.js @@ -1,6 +1,71 @@ (function (exports) { 'use strict'; + /** + * We define here a simple event bus: it can + * - emit events + * - add/remove listeners. + * + * This is a useful pattern of communication in many cases. For OWL, each + * components and stores are event buses. + */ + //------------------------------------------------------------------------------ + // EventBus + //------------------------------------------------------------------------------ + class EventBus { + constructor() { + this.subscriptions = {}; + } + /** + * Add a listener for the 'eventType' events. + * + * Note that the 'owner' of this event can be anything, but will more likely + * be a component or a class. The idea is that the callback will be called with + * the proper owner bound. + * + * Also, the owner should be kind of unique. This will be used to remove the + * listener. + */ + on(eventType, owner, callback) { + if (!callback) { + throw new Error("Missing callback"); + } + if (!this.subscriptions[eventType]) { + this.subscriptions[eventType] = []; + } + this.subscriptions[eventType].push({ + owner, + callback + }); + } + /** + * Remove a listener + */ + off(eventType, owner) { + const subs = this.subscriptions[eventType]; + if (subs) { + this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); + } + } + /** + * Emit an event of type 'eventType'. Any extra arguments will be passed to + * the listeners callback. + */ + trigger(eventType, ...args) { + const subs = this.subscriptions[eventType] || []; + for (let i = 0, iLen = subs.length; i < iLen; i++) { + const sub = subs[i]; + sub.callback.call(sub.owner, ...args); + } + } + /** + * Remove all subscriptions. + */ + clear() { + this.subscriptions = {}; + } + } + /** * Owl Observer * @@ -10,55 +75,13 @@ * This is a Observer class that can observe any JS values. The way it works * can be summarized thusly: * - primitive values are not observed at all - * - Objects are observed by replacing all their keys with getters/setters - * (recursively) - * - Arrays are observed by replacing their prototype with a customized version, - * which wrap some methods to allow the tracking of each state change. + * - Objects and arrays are observed by replacing them with a Proxy + * - each object/array metadata are tracked in a weakmap, and keep a revision + * number * - * Note that this code is inspired by Vue. + * Note that this code is loosely inspired by Vue. */ //------------------------------------------------------------------------------ - // Modified Array prototype - //------------------------------------------------------------------------------ - // we define here a new modified Array prototype, which basically override all - // Array methods that change some state to be able to track their changes - const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]; - const methodLen = methodsToPatch.length; - const ArrayProto = Array.prototype; - const ModifiedArrayProto = Object.create(ArrayProto); - for (let i = 0; i < methodLen; i++) { - const method = methodsToPatch[i]; - const initialMethod = ArrayProto[method]; - ModifiedArrayProto[method] = function (...args) { - if (!this.__observer__.allowMutations) { - throw new Error(`Array cannot be changed here")`); - } - this.__observer__.rev++; - this.__observer__.notifyChange(); - this.__owl__.rev++; - let parent = this; - do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent)); - let inserted; - switch (method) { - case "push": - case "unshift": - inserted = args; - break; - case "splice": - inserted = args.slice(2); - break; - } - if (inserted) { - for (let i = 0, iLen = inserted.length; i < iLen; i++) { - this.__observer__.observe(inserted[i], this); - } - } - return initialMethod.call(this, ...args); - }; - } - //------------------------------------------------------------------------------ // Observer //------------------------------------------------------------------------------ class Observer { @@ -66,123 +89,701 @@ this.rev = 1; this.allowMutations = true; this.dirty = false; - } - static set(target, key, value) { - if (!target.__owl__) { - throw Error("`Observer.set()` can only be called with observed Objects or Arrays"); - } - target.__owl__.observer.set(target, key, value); - } - static delete(target, key) { - if (!target.__owl__) { - throw Error("`Observer.delete()` can only be called with observed Objects"); - } - target.__owl__.observer.delete(target, key); + this.weakMap = new WeakMap(); } notifyCB() { } - notifyChange() { - this.dirty = true; - Promise.resolve().then(() => { - if (this.dirty) { - this.dirty = false; - this.notifyCB(); - } - }); - } observe(value, parent) { - if (value === null) { + if (value === null || typeof value !== "object" || value instanceof Date) { // fun fact: typeof null === 'object' - return; - } - if (typeof value !== "object") { - return; - } - if ("__owl__" in value) { - // already observed - value.__owl__.parent = parent; - return; - } - if (Array.isArray(value)) { - this._observeArr(value, parent); - } - else { - this._observeObj(value, parent); + return value; } + let metadata = this.weakMap.get(value) || this._observe(value, parent); + return metadata.proxy; } - set(target, key, value) { - let alreadyDefined = key in target && Object.getOwnPropertyDescriptor(target, key).configurable === false; - if (alreadyDefined) { - target[key] = value; - } - else { - this._addProp(target, key, value); - this._updateRevNumber(target); - } - this.notifyChange(); + revNumber(value) { + const metadata = this.weakMap.get(value); + return metadata ? metadata.rev : 0; } - delete(target, key) { - delete target[key]; - this._updateRevNumber(target); - this.notifyChange(); - } - _observeObj(obj, parent) { - obj.__owl__ = { - rev: this.rev, - deepRev: this.rev, - parent, - observer: this - }; - Object.defineProperty(obj, "__owl__", { enumerable: false }); - for (let key in obj) { - this._addProp(obj, key, obj[key]); - } - } - _observeArr(arr, parent) { - arr.__owl__ = { - rev: this.rev, - deepRev: this.rev, - parent, - observer: this - }; - Object.defineProperty(arr, "__owl__", { enumerable: false }); - arr.__proto__ = Object.create(ModifiedArrayProto); - arr.__proto__.__observer__ = this; - for (let i = 0, iLen = arr.length; i < iLen; i++) { - this.observe(arr[i], arr); - } - } - _addProp(obj, key, value) { + _observe(value, parent) { var self = this; - Object.defineProperty(obj, key, { - configurable: true, - enumerable: true, - get() { - return value; + const proxy = new Proxy(value, { + get(target, k) { + const targetValue = target[k]; + return self.observe(targetValue, value); }, - set(newVal) { + set(target, key, newVal) { + const value = target[key]; if (newVal !== value) { if (!self.allowMutations) { throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); } - self._updateRevNumber(obj); - value = newVal; - self.observe(newVal, obj); - self.notifyChange(); + self._updateRevNumber(target); + target[key] = newVal; + self.notifyCB(); } + return true; + }, + deleteProperty(target, key) { + if (key in target) { + delete target[key]; + self._updateRevNumber(target); + self.notifyCB(); + } + return true; } }); - this.observe(value, obj); + const metadata = { + value, + proxy, + rev: this.rev, + parent + }; + this.weakMap.set(value, metadata); + this.weakMap.set(metadata.proxy, metadata); + return metadata; } _updateRevNumber(target) { this.rev++; - target.__owl__.rev++; + let metadata = this.weakMap.get(target); let parent = target; do { - parent.__owl__.deepRev++; - } while ((parent = parent.__owl__.parent) && parent !== target); + metadata = this.weakMap.get(parent); + metadata.rev++; + } while ((parent = metadata.parent) && parent !== target); } } + /** + * Owl QWeb Expression Parser + * + * Owl needs in various contexts to be able to understand the structure of a + * string representing a javascript expression. The usual goal is to be able + * to rewrite some variables. For example, if a template has + * + * ```xml + * ... + * ``` + * + * this needs to be translated in something like this: + * + * ```js + * if (context["computeSomething"]({val: context["state"].val})) { ... } + * ``` + * + * This file contains the implementation of an extremely naive tokenizer/parser + * and evaluator for javascript expressions. The supported grammar is basically + * only expressive enough to understand the shape of objects, of arrays, and + * various operators. + */ + //------------------------------------------------------------------------------ + // Misc types, constants and helpers + //------------------------------------------------------------------------------ + const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(","); + const WORD_REPLACEMENT = { + and: "&&", + or: "||", + gt: ">", + gte: ">=", + lt: "<", + lte: "<=" + }; + const STATIC_TOKEN_MAP = { + "{": "LEFT_BRACE", + "}": "RIGHT_BRACE", + "[": "LEFT_BRACKET", + "]": "RIGHT_BRACKET", + ":": "COLON", + ",": "COMMA", + "(": "LEFT_PAREN", + ")": "RIGHT_PAREN" + }; + // note that the space after typeof is relevant. It makes sure that the formatted + // expression has a space after typeof + const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(","); + let tokenizeString = function (expr) { + let s = expr[0]; + let start = s; + if (s !== "'" && s !== '"') { + return false; + } + let i = 1; + let cur; + while (expr[i] && expr[i] !== start) { + cur = expr[i]; + s += cur; + if (cur === "\\") { + i++; + cur = expr[i]; + if (!cur) { + throw new Error("Invalid expression"); + } + s += cur; + } + i++; + } + if (expr[i] !== start) { + throw new Error("Invalid expression"); + } + s += start; + return { type: "VALUE", value: s }; + }; + let tokenizeNumber = function (expr) { + let s = expr[0]; + if (s && s.match(/[0-9]/)) { + let i = 1; + while (expr[i] && expr[i].match(/[0-9]|\./)) { + s += expr[i]; + i++; + } + return { type: "VALUE", value: s }; + } + else { + return false; + } + }; + let tokenizeSymbol = function (expr) { + let s = expr[0]; + if (s && s.match(/[a-zA-Z_\$]/)) { + let i = 1; + while (expr[i] && expr[i].match(/\w/)) { + s += expr[i]; + i++; + } + if (s in WORD_REPLACEMENT) { + return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length }; + } + return { type: "SYMBOL", value: s }; + } + else { + return false; + } + }; + const tokenizeStatic = function (expr) { + const char = expr[0]; + if (char && char in STATIC_TOKEN_MAP) { + return { type: STATIC_TOKEN_MAP[char], value: char }; + } + return false; + }; + const tokenizeOperator = function (expr) { + for (let op of OPERATORS) { + if (expr.startsWith(op)) { + return { type: "OPERATOR", value: op }; + } + } + return false; + }; + const TOKENIZERS = [ + tokenizeString, + tokenizeNumber, + tokenizeOperator, + tokenizeSymbol, + tokenizeStatic + ]; + /** + * Convert a javascript expression (as a string) into a list of tokens. For + * example: `tokenize("1 + b")` will return: + * ```js + * [ + * {type: "VALUE", value: "1"}, + * {type: "OPERATOR", value: "+"}, + * {type: "SYMBOL", value: "b"} + * ] + * ``` + */ + function tokenize(expr) { + const result = []; + let token = true; + while (token) { + expr = expr.trim(); + if (expr) { + for (let tokenizer of TOKENIZERS) { + token = tokenizer(expr); + if (token) { + result.push(token); + expr = expr.slice(token.size || token.value.length); + break; + } + } + } + else { + token = false; + } + } + if (expr.length) { + throw new Error(`Tokenizer error: could not tokenize "${expr}"`); + } + return result; + } + //------------------------------------------------------------------------------ + // Expression "evaluator" + //------------------------------------------------------------------------------ + /** + * 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 + * proper lookups in the context. + * + * Usually, this kind of code would be very simple to do if we had an AST (so, + * if we had a javascript parser), since then, we would only need to find the + * variables and replace them. However, a parser is more complicated, and there + * are no standard builtin parser API. + * + * Since this method is applied to simple javasript expressions, and the work to + * be done is actually quite simple, we actually can get away with not using a + * parser, which helps with the code size. + * + * Here is the heuristic used by this method to determine if a token is a + * variable: + * - by default, all symbols are considered a variable + * - unless the previous token is a dot (in that case, this is a property: `a.b`) + * - or if the previous token is a left brace or a comma, and the next token is + * a colon (in that case, this is an object key: `{a: b}`) + */ + function compileExpr(expr, vars) { + const tokens = tokenize(expr); + let result = ""; + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { + // we need to find if it is a variable + let isVar = true; + let prevToken = tokens[i - 1]; + if (prevToken) { + if (prevToken.type === "OPERATOR" && prevToken.value === ".") { + isVar = false; + } + else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { + let nextToken = tokens[i + 1]; + if (nextToken && nextToken.type === "COLON") { + isVar = false; + } + } + } + if (isVar) { + if (token.value in vars && "id" in vars[token.value]) { + token.value = vars[token.value].id; + } + else { + token.value = `context['${token.value}']`; + } + } + } + result += token.value; + } + return result; + } + + const INTERP_REGEXP = /\{\{.*?\}\}/g; + //------------------------------------------------------------------------------ + // Compilation Context + //------------------------------------------------------------------------------ + class CompilationContext { + constructor(name) { + this.code = []; + this.variables = {}; + this.escaping = false; + this.parentNode = null; + this.parentTextNode = null; + this.rootNode = null; + this.indentLevel = 0; + this.shouldDefineOwner = false; + this.shouldDefineParent = false; + this.shouldDefineQWeb = false; + this.shouldDefineUtils = false; + this.shouldDefineRefs = false; + this.shouldDefineResult = true; + this.shouldProtectContext = false; + this.shouldTrackScope = false; + this.loopNumber = 0; + this.inPreTag = false; + this.allowMultipleRoots = false; + this.hasParentWidget = false; + this.scopeVars = []; + this.currentKey = ""; + this.templates = {}; + this.callingLevel = 0; + this.inliningLevel = 0; + this.rootContext = this; + this.templateName = name || "noname"; + this.templates[this.templateName] = true; + this.addLine("var h = this.h;"); + } + generateID() { + return CompilationContext.nextID++; + } + /** + * This method generates a "template key", which is basically a unique key + * which depends on the currently set keys, and on the iteration numbers (if + * we are in a loop). + * + * Such a key is necessary when we need to associate an id to some element + * generated by a template (for example, a component) + */ + generateTemplateKey() { + const id = this.generateID(); + let locationExpr = `\`__${this.generateID()}__`; + for (let i = 0; i < this.loopNumber - 1; i++) { + locationExpr += `\${i${i + 1}}__`; + } + if (this.currentKey) { + const k = this.currentKey; + this.addLine(`let k${id} = ${locationExpr}\` + ${k};`); + } + else { + locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`"; + this.addLine(`let k${id} = ${locationExpr};`); + } + return `k${id}`; + } + generateCode() { + const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length; + if (shouldTrackScope) { + // add some vars to scope if needed + for (let scopeVar of this.scopeVars.reverse()) { + let { index, key, indent } = scopeVar; + const prefix = new Array(indent + 2).join(" "); + this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`); + } + this.code.unshift(" const scope = Object.create(null);"); + } + if (this.shouldProtectContext) { + this.code.unshift(" context = Object.create(context);"); + } + if (this.shouldDefineResult) { + this.code.unshift(" let result;"); + } + if (this.shouldDefineRefs) { + this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); + } + if (this.shouldDefineOwner) { + // this is necessary to prevent some directives (t-forach for ex) to + // pollute the rendering context by adding some keys in it. + this.code.unshift(" let owner = context;"); + } + if (this.shouldDefineParent) { + if (this.hasParentWidget) { + this.code.unshift(" let parent = extra.parent;"); + } + else { + this.code.unshift(" let parent = context;"); + } + } + if (this.shouldDefineQWeb) { + this.code.unshift(" let QWeb = this.constructor;"); + } + if (this.shouldDefineUtils) { + this.code.unshift(" let utils = this.constructor.utils;"); + } + return this.code; + } + withParent(node) { + if (!this.allowMultipleRoots && + this === this.rootContext && + (this.parentNode || this.parentTextNode)) { + throw new Error("A template should not have more than one root node"); + } + if (!this.rootContext.rootNode) { + this.rootContext.rootNode = node; + } + if (!this.parentNode && this.rootContext.shouldDefineResult) { + this.addLine(`result = vn${node};`); + } + return this.subContext("parentNode", node); + } + subContext(key, value) { + const newContext = Object.create(this); + newContext[key] = value; + if (key === "caller") { + newContext.callingLevel++; + newContext.inliningLevel++; + } + return newContext; + } + indent() { + this.indentLevel++; + } + dedent() { + this.indentLevel--; + } + addLine(line) { + const prefix = new Array(this.indentLevel + 2).join(" "); + this.code.push(prefix + line); + return this.code.length - 1; + } + addToScope(key, expr) { + const index = this.addLine(`context.${key} = ${expr};`); + this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel }); + } + addIf(condition) { + this.addLine(`if (${condition}) {`); + this.indent(); + } + addElse() { + this.dedent(); + this.addLine("} else {"); + this.indent(); + } + closeIf() { + this.dedent(); + this.addLine("}"); + } + /** + * Recursively (inverse) fetches the `caller` of a context + * Useful to determine to which t-call a t-raw="0" refers + */ + getCaller(targetLevel) { + if (targetLevel === undefined) { + targetLevel = this.inliningLevel; + } + if (targetLevel === this.callingLevel) { + return this.caller || null; + } + const proto = this.__proto__; + return proto ? proto.getCaller(targetLevel) : null; + } + /** + * Marks the context with the current recursive level + * in which we are for inlining archs (t-raw="0") + */ + getInliningContext() { + return this.subContext("inliningLevel", this.inliningLevel - 1); + } + getValue(val) { + return val in this.variables ? this.getValue(this.variables[val]) : val; + } + /** + * Prepare an expression for being consumed at render time. Its main job + * is to + * - replace unknown variables by a lookup in the context + * - replace already defined variables by their internal name + */ + formatExpression(expr) { + return compileExpr(expr, this.variables); + } + /** + * Perform string interpolation on the given string. Note that if the whole + * string is an expression, it simply returns it (formatted and enclosed in + * parentheses). + * For instance: + * 'Hello {{x}}!' -> `Hello ${x}` + * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') + */ + interpolate(s) { + let matches = s.match(INTERP_REGEXP); + if (matches && matches[0].length === s.length) { + return `(${this.formatExpression(s.slice(2, -2))})`; + } + let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); + return "`" + r + "`"; + } + } + CompilationContext.nextID = 1; + + //------------------------------------------------------------------------------ + // module/props.ts + //------------------------------------------------------------------------------ + function updateProps(oldVnode, vnode) { + var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; + if (!oldProps && !props) + return; + if (oldProps === props) + return; + oldProps = oldProps || {}; + props = props || {}; + for (key in oldProps) { + if (!props[key]) { + delete elm[key]; + } + } + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== "value" || elm[key] !== cur)) { + elm[key] = cur; + } + } + } + const propsModule = { + create: updateProps, + update: updateProps + }; + //------------------------------------------------------------------------------ + // module/eventlisteners.ts + //------------------------------------------------------------------------------ + function invokeHandler(handler, vnode, event) { + if (typeof handler === "function") { + // call function handler + handler.call(vnode, event, vnode); + } + else if (typeof handler === "object") { + // call handler with arguments + if (typeof handler[0] === "function") { + // special case for single argument for performance + if (handler.length === 2) { + handler[0].call(vnode, handler[1], event, vnode); + } + else { + var args = handler.slice(1); + args.push(event); + args.push(vnode); + handler[0].apply(vnode, args); + } + } + else { + // call multiple handlers + for (let i = 0, iLen = handler.length; i < iLen; i++) { + invokeHandler(handler[i], vnode, event); + } + } + } + } + function handleEvent(event, vnode) { + var name = event.type, on = vnode.data.on; + // call event handler(s) if exists + if (on && on[name]) { + invokeHandler(on[name], vnode, event); + } + } + function createListener() { + return function handler(event) { + handleEvent(event, handler.vnode); + }; + } + function updateEventListeners(oldVnode, vnode) { + var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; + // optimization for reused immutable handlers + if (oldOn === on) { + return; + } + // remove existing listeners which no longer used + if (oldOn && oldListener) { + // if element changed or deleted we remove all existing listeners unconditionally + if (!on) { + for (name in oldOn) { + // remove listener if element was changed or existing listeners removed + oldElm.removeEventListener(name, oldListener, false); + } + } + else { + for (name in oldOn) { + // remove listener if existing listener removed + if (!on[name]) { + oldElm.removeEventListener(name, oldListener, false); + } + } + } + } + // add new listeners which has not already attached + if (on) { + // reuse existing listener or create new + var listener = (vnode.listener = oldVnode.listener || createListener()); + // update vnode for listener + listener.vnode = vnode; + // if element changed or added we add all needed listeners unconditionally + if (!oldOn) { + for (name in on) { + // add listener if element was changed or new listeners added + elm.addEventListener(name, listener, false); + } + } + else { + for (name in on) { + // add listener if new listener added + if (!oldOn[name]) { + elm.addEventListener(name, listener, false); + } + } + } + } + } + const eventListenersModule = { + create: updateEventListeners, + update: updateEventListeners, + destroy: updateEventListeners + }; + //------------------------------------------------------------------------------ + // attributes.ts + //------------------------------------------------------------------------------ + const xlinkNS = "http://www.w3.org/1999/xlink"; + const xmlNS = "http://www.w3.org/XML/1998/namespace"; + const colonChar = 58; + const xChar = 120; + function updateAttrs(oldVnode, vnode) { + var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; + if (!oldAttrs && !attrs) + return; + if (oldAttrs === attrs) + return; + oldAttrs = oldAttrs || {}; + attrs = attrs || {}; + // update modified attributes, add new attributes + for (key in attrs) { + const cur = attrs[key]; + const old = oldAttrs[key]; + if (old !== cur) { + if (cur === true) { + elm.setAttribute(key, ""); + } + else if (cur === false) { + elm.removeAttribute(key); + } + else { + if (key.charCodeAt(0) !== xChar) { + elm.setAttribute(key, cur); + } + else if (key.charCodeAt(3) === colonChar) { + // Assume xml namespace + elm.setAttributeNS(xmlNS, key, cur); + } + else if (key.charCodeAt(5) === colonChar) { + // Assume xlink namespace + elm.setAttributeNS(xlinkNS, key, cur); + } + else { + elm.setAttribute(key, cur); + } + } + } + } + // remove removed attributes + // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) + // the other option is to remove all attributes with value == undefined + for (key in oldAttrs) { + if (!(key in attrs)) { + elm.removeAttribute(key); + } + } + } + const attrsModule = { + create: updateAttrs, + update: updateAttrs + }; + //------------------------------------------------------------------------------ + // class.ts + //------------------------------------------------------------------------------ + function updateClass(oldVnode, vnode) { + var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class; + if (!oldClass && !klass) + return; + if (oldClass === klass) + return; + oldClass = oldClass || {}; + klass = klass || {}; + elm = vnode.elm; + for (name in oldClass) { + if (!klass[name]) { + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + elm.classList[cur ? "add" : "remove"](name); + } + } + } + const classModule = { create: updateClass, update: updateClass }; + /** * Owl VDOM * @@ -273,20 +874,11 @@ vnode.elm = api.createComment(vnode.text); } else if (sel !== undefined) { - // Parse selector - const hashIdx = sel.indexOf("#"); - const dotIdx = sel.indexOf(".", hashIdx); - const hash = hashIdx > 0 ? hashIdx : sel.length; - const dot = dotIdx > 0 ? dotIdx : sel.length; - const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; - const elm = (vnode.elm = - isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, tag) - : api.createElement(tag)); - if (hash < dot) - elm.setAttribute("id", sel.slice(hash + 1, dot)); - if (dotIdx > 0) - elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); + const elm = vnode.elm || + (vnode.elm = + isDef(data) && isDef((i = data.ns)) + ? api.createElementNS(i, sel) + : api.createElement(sel)); for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode); if (array(children)) { @@ -557,18 +1149,6 @@ function setTextContent(node, text) { node.textContent = text; } - function getTextContent(node) { - return node.textContent; - } - function isElement(node) { - return node.nodeType === 1; - } - function isText(node) { - return node.nodeType === 3; - } - function isComment(node) { - return node.nodeType === 8; - } const htmlDomApi = { createElement, createElementNS, @@ -580,19 +1160,21 @@ parentNode, nextSibling, tagName, - setTextContent, - getTextContent, - isElement, - isText, - isComment + setTextContent }; function addNS(data, children, sel) { + if (sel === "dummy") { + // we do not need to add the namespace on dummy elements, they come from a + // subcomponent, which will handle the namespace itself + return; + } data.ns = "http://www.w3.org/2000/svg"; if (sel !== "foreignObject" && children !== undefined) { for (let i = 0, iLen = children.length; i < iLen; ++i) { - let childData = children[i].data; + const child = children[i]; + let childData = child.data; if (childData !== undefined) { - addNS(childData, children[i].children, children[i].sel); + addNS(childData, child.children, child.sel); } } } @@ -631,517 +1213,134 @@ children[i] = vnode(undefined, undefined, undefined, children[i], undefined); } } - if (sel[0] === "s" && - sel[1] === "v" && - sel[2] === "g" && - (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { - addNS(data, children, sel); - } return vnode(sel, data, children, text, undefined); } - function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; - if (!oldProps && !props) - return; - if (oldProps === props) - return; - oldProps = oldProps || {}; - props = props || {}; - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - const propsModule = { - create: updateProps, - update: updateProps - }; - function invokeHandler(handler, vnode, event) { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } - else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); - } - else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); - } - } - else { - // call multiple handlers - for (let i = 0, iLen = handler.length; i < iLen; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } - } - function handleEvent(event, vnode) { - var name = event.type, on = vnode.data.on; - // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } - } - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - oldElm.removeEventListener(name, oldListener, false); - } - } - else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = (vnode.listener = oldVnode.listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - elm.addEventListener(name, listener, false); - } - } - else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } - } - const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners - }; - const xlinkNS = "http://www.w3.org/1999/xlink"; - const xmlNS = "http://www.w3.org/XML/1998/namespace"; - const colonChar = 58; - const xChar = 120; - function updateAttrs(oldVnode, vnode) { - var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) - return; - if (oldAttrs === attrs) - return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } - else if (cur === false) { - elm.removeAttribute(key); - } - else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } - else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } - else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } - else { - elm.setAttribute(key, cur); - } - } - } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } - } - const attrsModule = { - create: updateAttrs, - update: updateAttrs - }; - //------------------------------------------------------------------------------ - // class.ts - //------------------------------------------------------------------------------ - function updateClass(oldVnode, vnode) { - var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class; - if (!oldClass && !klass) - return; - if (oldClass === klass) - return; - oldClass = oldClass || {}; - klass = klass || {}; - elm = vnode.elm; - for (name in oldClass) { - if (!klass[name]) { - elm.classList.remove(name); - } - } - for (name in klass) { - cur = klass[name]; - if (cur !== oldClass[name]) { - elm.classList[cur ? "add" : "remove"](name); - } - } - } - const classModule = { create: updateClass, update: updateClass }; - //------------------------------------------------------------------------------ - // patch - //------------------------------------------------------------------------------ + const patch = init([eventListenersModule, attrsModule, propsModule, classModule]); /** - * Owl QWeb Expression Parser + * Owl Utils * - * Owl needs in various contexts to be able to understand the structure of a - * string representing a javascript expression. The usual goal is to be able - * to rewrite some variables. For example, if a template has + * We have here a small collection of utility functions: * - * ```xml - * ... - * ``` - * - * this needs to be translated in something like this: - * - * ```js - * if (context["computeSomething"]({val: context["state"].val})) { ... } - * ``` - * - * This file contains the implementation of an extremely naive tokenizer/parser - * and evaluator for javascript expressions. The supported grammar is basically - * only expressive enough to understand the shape of objects, of arrays, and - * various operators. + * - whenReady + * - loadJS + * - loadFile + * - escape + * - debounce */ - //------------------------------------------------------------------------------ - // Misc types, constants and helpers - //------------------------------------------------------------------------------ - const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(","); - const WORD_REPLACEMENT = { - and: "&&", - or: "||", - gt: ">", - gte: ">=", - lt: "<", - lte: "<=" - }; - const STATIC_TOKEN_MAP = { - "{": "LEFT_BRACE", - "}": "RIGHT_BRACE", - "[": "LEFT_BRACKET", - "]": "RIGHT_BRACKET", - ":": "COLON", - ",": "COMMA", - "(": "LEFT_PAREN", - ")": "RIGHT_PAREN" - }; - const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(","); - let tokenizeString = function (expr) { - let s = expr[0]; - let start = s; - if (s !== "'" && s !== '"') { - return false; - } - let i = 1; - let cur; - while (expr[i] && expr[i] !== start) { - cur = expr[i]; - s += cur; - if (cur === "\\") { - i++; - cur = expr[i]; - if (!cur) { - throw new Error("Invalid expression"); - } - s += cur; - } - i++; - } - if (expr[i] !== start) { - throw new Error("Invalid expression"); - } - s += start; - return { type: "VALUE", value: s }; - }; - let tokenizeNumber = function (expr) { - let s = expr[0]; - if (s && s.match(/[0-9]/)) { - let i = 1; - while (expr[i] && expr[i].match(/[0-9]|\./)) { - s += expr[i]; - i++; - } - return { type: "VALUE", value: s }; - } - else { - return false; - } - }; - let tokenizeSymbol = function (expr) { - let s = expr[0]; - if (s && s.match(/[a-zA-Z_\$]/)) { - let i = 1; - while (expr[i] && expr[i].match(/\w/)) { - s += expr[i]; - i++; - } - if (s in WORD_REPLACEMENT) { - return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length }; - } - return { type: "SYMBOL", value: s }; - } - else { - return false; - } - }; - const tokenizeStatic = function (expr) { - const char = expr[0]; - if (char && char in STATIC_TOKEN_MAP) { - return { type: STATIC_TOKEN_MAP[char], value: char }; - } - return false; - }; - const tokenizeOperator = function (expr) { - for (let op of OPERATORS) { - if (expr.startsWith(op)) { - return { type: "OPERATOR", value: op }; - } - } - return false; - }; - const TOKENIZERS = [ - tokenizeString, - tokenizeNumber, - tokenizeSymbol, - tokenizeStatic, - tokenizeOperator - ]; - /** - * Convert a javascript expression (as a string) into a list of tokens. For - * example: `tokenize("1 + b")` will return: - * ```js - * [ - * {type: "VALUE", value: "1"}, - * {type: "OPERATOR", value: "+"}, - * {type: "SYMBOL", value: "b"} - * ] - * ``` - */ - function tokenize(expr) { - const result = []; - let token = true; - while (token) { - expr = expr.trim(); - if (expr) { - for (let tokenizer of TOKENIZERS) { - token = tokenizer(expr); - if (token) { - result.push(token); - expr = expr.slice(token.size || token.value.length); - break; - } - } + function whenReady(fn) { + return new Promise(function (resolve) { + if (document.readyState !== "loading") { + resolve(); } else { - token = false; + document.addEventListener("DOMContentLoaded", resolve, false); } - } - if (expr.length) { - throw new Error(`Tokenizer error: could not tokenize "${expr}"`); - } - return result; + }).then(fn || function () { }); + } + const loadedScripts = {}; + function loadJS(url) { + if (url in loadedScripts) { + return loadedScripts[url]; + } + const promise = new Promise(function (resolve, reject) { + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = url; + script.onload = function () { + resolve(); + }; + script.onerror = function () { + reject(`Error loading file '${url}'`); + }; + const head = document.head || document.getElementsByTagName("head")[0]; + head.appendChild(script); + }); + loadedScripts[url] = promise; + return promise; + } + async function loadFile(url) { + const result = await fetch(url); + if (!result.ok) { + throw new Error("Error while fetching xml templates"); + } + return await result.text(); + } + function escape(str) { + if (str === undefined) { + return ""; + } + if (typeof str === "number") { + return String(str); + } + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, "'") + .replace(/`/g, "`"); } - //------------------------------------------------------------------------------ - // Expression "evaluator" - //------------------------------------------------------------------------------ /** - * 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 - * proper lookups in the context. + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. * - * Usually, this kind of code would be very simple to do if we had an AST (so, - * if we had a javascript parser), since then, we would only need to find the - * variables and replace them. However, a parser is more complicated, and there - * are no standard builtin parser API. - * - * Since this method is applied to simple javasript expressions, and the work to - * be done is actually quite simple, we actually can get away with not using a - * parser, which helps with the code size. - * - * Here is the heuristic used by this method to determine if a token is a - * variable: - * - by default, all symbols are considered a variable - * - unless the previous token is a dot (in that case, this is a property: `a.b`) - * - or if the previous token is a left brace or a comma, and the next token is - * a colon (in that case, this is an object key: `{a: b}`) + * Inspired by https://davidwalsh.name/javascript-debounce-function */ - function compileExpr(expr, vars) { - const tokens = tokenize(expr); - let result = ""; - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { - // we need to find if it is a variable - let isVar = true; - let prevToken = tokens[i - 1]; - if (prevToken) { - if (prevToken.type === "OPERATOR" && prevToken.value === ".") { - isVar = false; - } - else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { - let nextToken = tokens[i + 1]; - if (nextToken && nextToken.type === "COLON") { - isVar = false; - } - } - } - if (isVar) { - if (token.value in vars && "id" in vars[token.value]) { - token.value = vars[token.value].id; - } - else { - token.value = `context['${token.value}']`; - } + function debounce(func, wait, immediate) { + let timeout; + return function () { + const context = this; + const args = arguments; + function later() { + timeout = null; + if (!immediate) { + func.apply(context, args); } } - result += token.value; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + } + function shallowEqual(p1, p2) { + for (let k in p1) { + if (p1[k] !== p2[k]) { + return false; + } } - return result; + return true; } - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a component or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } + var _utils = /*#__PURE__*/Object.freeze({ + __proto__: null, + whenReady: whenReady, + loadJS: loadJS, + loadFile: loadFile, + escape: escape, + debounce: debounce, + shallowEqual: shallowEqual + }); //------------------------------------------------------------------------------ // Const/global stuff/helpers //------------------------------------------------------------------------------ const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"]; + const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; - const DIRECTIVE_NAMES = { - name: 1, - att: 1, - attf: 1, - key: 1 - }; - const DIRECTIVES = []; const NODE_HOOKS_PARAMS = { create: "(_, n)", insert: "vn", - remove: "(vn, rm)" + remove: "(vn, rm)", + destroy: "()" }; const UTILS = { - h: h, toObj(expr) { if (typeof expr === "string") { expr = expr.trim(); @@ -1157,58 +1356,89 @@ } return expr; }, - shallowEqual(p1, p2) { - for (let k in p1) { - if (p1[k] !== p2[k]) { - return false; - } - } - return true; + shallowEqual, + addNameSpace(vnode) { + addNS(vnode.data, vnode.children, vnode.sel); } }; function parseXML(xml) { const parser = new DOMParser(); const doc = parser.parseFromString(xml, "text/xml"); if (doc.getElementsByTagName("parsererror").length) { - throw new Error("Invalid XML in template"); + let msg = "Invalid XML in template."; + const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent; + if (parsererrorText) { + msg += "\nThe parser has produced the following error message:\n" + parsererrorText; + const re = /\d+/g; + const firstMatch = re.exec(parsererrorText); + if (firstMatch) { + const lineNumber = Number(firstMatch[0]); + const line = xml.split("\n")[lineNumber - 1]; + const secondMatch = re.exec(parsererrorText); + if (line && secondMatch) { + const columnIndex = Number(secondMatch[0]) - 1; + if (line[columnIndex]) { + msg += + `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` + + `${line}\n${"-".repeat(columnIndex - 1)}^`; + } + } + } + } + throw new Error(msg); } return doc; } - let nextID = 1; //------------------------------------------------------------------------------ // QWeb rendering engine //------------------------------------------------------------------------------ class QWeb extends EventBus { - constructor(data) { + constructor(config = {}) { super(); - this.templates = {}; - this.utils = UTILS; - // the id field is useful to be able to hash qweb instances. The current - // use case is that component's templates are qweb dependant, and need to be - // able to map a qweb instance to a template name. - this.id = nextID++; - // slots contains sub templates defined with t-set inside t-component nodes, and - // are meant to be used by the t-slot directive. - this.slots = {}; - this.nextSlotId = 1; - if (data) { - this.addTemplates(data); + this.h = h; + // recursiveTemplates contains sub templates called with t-call, but which + // ends up in recursive situations. This is very similar to the slot situation, + // as in we need to propagate the scope. + this.recursiveFns = {}; + this.isUpdating = false; + this.templates = Object.create(QWeb.TEMPLATES); + if (config.templates) { + this.addTemplates(config.templates); + } + if (config.translateFn) { + this.translateFn = config.translateFn; } } static addDirective(directive) { - DIRECTIVES.push(directive); - DIRECTIVE_NAMES[directive.name] = 1; - DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); + if (directive.name in QWeb.DIRECTIVE_NAMES) { + throw new Error(`Directive "${directive.name} already registered`); + } + QWeb.DIRECTIVES.push(directive); + QWeb.DIRECTIVE_NAMES[directive.name] = 1; + QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority); if (directive.extraNames) { - directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1)); + directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1)); } } - static register(name, Component) { + static registerComponent(name, Component) { if (QWeb.components[name]) { throw new Error(`Component '${name}' has already been registered`); } QWeb.components[name] = Component; } + /** + * Register globally a template. All QWeb instances will obtain their + * templates from their own template map, and then, from the global static + * TEMPLATES property. + */ + static registerTemplate(name, template) { + if (QWeb.TEMPLATES[name]) { + throw new Error(`Template '${name}' has already been registered`); + } + const qweb = new QWeb(); + qweb.addTemplate(name, template); + QWeb.TEMPLATES[name] = qweb.templates[name]; + } /** * Add a template to the internal template map. Note that it is not * immediately compiled. @@ -1224,12 +1454,12 @@ this._addTemplate(name, doc.firstChild); } /** - * Load templates from a xml (as a string). This will look up for the first - * tag, and will consider each child of this as a template, with - * the name given by the t-name attribute. + * Load templates from a xml (as a string or xml document). This will look up + * for the first tag, and will consider each child of this as a + * template, with the name given by the t-name attribute. */ addTemplates(xmlstr) { - const doc = parseXML(xmlstr); + const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr; const templates = doc.getElementsByTagName("templates")[0]; if (!templates) { return; @@ -1246,7 +1476,7 @@ this._processTemplate(elem); const template = { elem, - fn: (context, extra) => { + fn: function (context, extra) { const compiledFunction = this._compile(name, elem); template.fn = compiledFunction; return compiledFunction.call(this, context, extra); @@ -1300,44 +1530,82 @@ } return template.fn.call(this, context, extra); } - _compile(name, elem, parentNode) { + /** + * Render a template to a html string. + * + * Note that this is more limited than the `render` method: it is not suitable + * to render a full component tree, since this is an asynchronous operation. + * This method can only render templates without components. + */ + renderToString(name, context = {}, extra) { + const vnode = this.render(name, context, extra); + if (vnode.sel === undefined) { + return vnode.text; + } + const node = document.createElement(vnode.sel); + const result = patch(node, vnode); + return result.elm.outerHTML; + } + /** + * Force all widgets connected to this QWeb instance to rerender themselves. + * + * This method is mostly useful for external code that want to modify the + * application in some cases. For example, a router plugin. + */ + forceUpdate() { + this.isUpdating = true; + Promise.resolve().then(() => { + if (this.isUpdating) { + this.isUpdating = false; + this.trigger("update"); + } + }); + } + _compile(name, elem, parentContext) { const isDebug = elem.attributes.hasOwnProperty("t-debug"); - const ctx = new Context(name); - if (parentNode) { - ctx.nextID = parentNode + 1; - ctx.parentNode = parentNode; + const ctx = new CompilationContext(name); + if (elem.tagName !== "t") { + ctx.shouldDefineResult = false; + } + if (parentContext) { + ctx.templates = Object.create(parentContext.templates); + ctx.variables = Object.create(parentContext.variables); + ctx.parentNode = parentContext.parentNode || ctx.generateID(); ctx.allowMultipleRoots = true; - ctx.addLine(`let c${parentNode} = extra.parentNode;`); + ctx.hasParentWidget = true; + ctx.shouldDefineResult = false; + ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); + for (let v in parentContext.variables) { + let variable = parentContext.variables[v]; + if (variable.id) { + ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`); + } + } + } + if (parentContext) { + ctx.addLine(" Object.assign(context, extra.fiber.scope);"); } this._compileNode(elem, ctx); - if (ctx.shouldProtectContext) { - ctx.code.unshift(" context = Object.create(context);"); - } - if (ctx.shouldDefineOwner) { - // this is necessary to prevent some directives (t-forach for ex) to - // pollute the rendering context by adding some keys in it. - ctx.code.unshift(" let owner = context;"); - } - if (ctx.shouldDefineQWeb) { - ctx.code.unshift(" let QWeb = this.constructor;"); - } - if (ctx.shouldDefineUtils) { - ctx.code.unshift(" let utils = this.utils;"); - } - if (!parentNode) { - if (!ctx.rootNode) { - throw new Error("A template should have one root node"); + if (!parentContext) { + if (ctx.shouldDefineResult) { + ctx.addLine(`return result;`); + } + else { + if (!ctx.rootNode) { + throw new Error(`A template should have one root node (${ctx.templateName})`); + } + ctx.addLine(`return vn${ctx.rootNode};`); } - ctx.addLine(`return vn${ctx.rootNode};`); } + let code = ctx.generateCode(); let template; try { - template = new Function("context", "extra", ctx.code.join("\n")); + template = new Function("context", "extra", code.join("\n")); } catch (e) { const templateName = ctx.templateName.replace(/`/g, "'"); console.groupCollapsed(`Invalid Code generated by ${templateName}`); - console.warn(ctx.code.join("\n")); + console.warn(code.join("\n")); console.groupEnd(); throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`); } @@ -1364,8 +1632,18 @@ } text = text.replace(whitespaceRE, " "); } + if (this.translateFn) { + if (node.parentNode.getAttribute("t-translation") !== "off") { + text = this.translateFn(text); + } + } if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); + if (node.nodeType === 3) { + ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); + } + else if (node.nodeType === 8) { + ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`); + } } else if (ctx.parentTextNode) { ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`); @@ -1375,35 +1653,52 @@ // template rendering. let nodeID = ctx.generateID(); ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); + ctx.addLine(`result = vn${nodeID};`); ctx.rootContext.rootNode = nodeID; ctx.rootContext.parentTextNode = nodeID; } return; } + if (ctx !== ctx.rootContext) { + ctx = ctx.subContext("currentKey", ctx.currentKey); + } const firstLetter = node.tagName[0]; if (firstLetter === firstLetter.toUpperCase()) { // this is a component, we modify in place the xml document to change // to node.setAttribute("t-component", node.tagName); } + else if (node.tagName !== "t" && node.hasAttribute("t-component")) { + throw new Error(`Directive 't-component' can only be used on nodes (used on a <${node.tagName}>)`); + } const attributes = node.attributes; const validDirectives = []; - let withHandlers = false; + const finalizers = []; // maybe this is not optimal: we iterate on all attributes here, and again // just after for each directive. for (let i = 0; i < attributes.length; i++) { let attrName = attributes[i].name; if (attrName.startsWith("t-")) { let dName = attrName.slice(2).split(/-|\./)[0]; - if (!(dName in DIRECTIVE_NAMES)) { + if (!(dName in QWeb.DIRECTIVE_NAMES)) { throw new Error(`Unknown QWeb directive: '${attrName}'`); } + if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) { + const tNode = document.createElement("t"); + tNode.setAttribute(attrName, node.getAttribute(attrName)); + for (let child of Array.from(node.childNodes)) { + tNode.appendChild(child); + } + node.appendChild(tNode); + node.removeAttribute(attrName); + } } } - const DIR_N = DIRECTIVES.length; + const DIR_N = QWeb.DIRECTIVES.length; const ATTR_N = attributes.length; + let withHandlers = false; for (let i = 0; i < DIR_N; i++) { - let directive = DIRECTIVES[i]; + let directive = QWeb.DIRECTIVES[i]; let fullName; let value; for (let j = 0; j < ATTR_N; j++) { @@ -1421,6 +1716,9 @@ } } for (let { directive, value, fullName } of validDirectives) { + if (directive.finalize) { + finalizers.push({ directive, value, fullName }); + } if (directive.atNodeEncounter) { const isDone = directive.atNodeEncounter({ node, @@ -1430,6 +1728,9 @@ value }); if (isDone) { + for (let { directive, value, fullName } of finalizers) { + directive.finalize({ node, qweb: this, ctx, fullName, value }); + } return; } } @@ -1471,10 +1772,18 @@ ctx = ctx.subContext("inPreTag", true); } this._compileChildren(node, ctx); - for (let { directive, value, fullName } of validDirectives) { - if (directive.finalize) { - directive.finalize({ node, qweb: this, ctx, fullName, value }); - } + // svg support + // we hadd svg namespace if it is a svg or if it is a g, but only if it is + // the root node. This is the easiest way to support svg sub components: + // they need to have a g tag as root. Otherwise, we would need a complete + // list of allowed svg tags. + const shouldAddNS = node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode); + if (shouldAddNS) { + ctx.rootContext.shouldDefineUtils = true; + ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`); + } + for (let { directive, value, fullName } of finalizers) { + directive.finalize({ node, qweb: this, ctx, fullName, value }); } } _compileGenericNode(node, ctx, withHandlers = true) { @@ -1510,7 +1819,10 @@ let classObj = ""; for (let i = 0; i < attributes.length; i++) { let name = attributes[i].name; - const value = attributes[i].textContent; + let value = attributes[i].textContent; + if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) { + value = this.translateFn(value); + } // regular attributes if (!name.startsWith("t-") && !node.getAttribute("t-attf-" + name)) { const attID = ctx.generateID(); @@ -1537,9 +1849,10 @@ if (name.startsWith("t-att-")) { let attName = name.slice(6); const v = ctx.getValue(value); - let formattedValue = v.id || ctx.formatExpression(v); + let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id; if (attName === "class") { - formattedValue = `this.utils.toObj(${formattedValue})`; + ctx.rootContext.shouldDefineUtils = true; + formattedValue = `utils.toObj(${formattedValue})`; if (classObj) { ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); } @@ -1594,13 +1907,7 @@ } } let nodeID = ctx.generateID(); - let nodeKey = node.getAttribute("t-key"); - if (nodeKey) { - nodeKey = ctx.formatExpression(nodeKey); - } - else { - nodeKey = nodeID; - } + let nodeKey = ctx.currentKey || nodeID; const parts = [`key:${nodeKey}`]; if (attrs.length + tattrs.length > 0) { parts.push(`attrs:{${attrs.join(",")}}`); @@ -1640,668 +1947,47 @@ } } } + QWeb.utils = UTILS; QWeb.components = Object.create(null); + QWeb.DIRECTIVE_NAMES = { + name: 1, + att: 1, + attf: 1, + translation: 1 + }; + QWeb.DIRECTIVES = []; + QWeb.TEMPLATES = {}; + QWeb.nextId = 1; // dev mode enables better error messages or more costly validations QWeb.dev = false; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class Context { - constructor(name) { - this.nextID = 1; - this.code = []; - this.variables = {}; - this.escaping = false; - this.parentNode = null; - this.parentTextNode = null; - this.rootNode = null; - this.indentLevel = 0; - this.shouldDefineOwner = false; - this.shouldDefineQWeb = false; - this.shouldDefineUtils = false; - this.shouldProtectContext = false; - this.inLoop = false; - this.inPreTag = false; - this.allowMultipleRoots = false; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("var h = this.utils.h;"); - } - generateID() { - const id = this.rootContext.nextID++; - return id; - } - withParent(node) { - if (!this.allowMultipleRoots && - this === this.rootContext && - (this.parentNode || this.parentTextNode)) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - return this.subContext("parentNode", node); - } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.indentLevel++; - } - dedent() { - this.indentLevel--; - } - addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - closeIf() { - this.dedent(); - this.addLine("}"); - } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - /** - * Prepare an expression for being consumed at render time. Its main job - * is to - * - replace unknown variables by a lookup in the context - * - replace already defined variables by their internal name - */ - formatExpression(expr) { - return compileExpr(expr, this.variables); - } - /** - * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted and enclosed in - * parentheses). - * For instance: - * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') - */ - interpolate(s) { - let matches = s.match(/\{\{.*?\}\}/g); - if (matches && matches[0].length === s.length) { - return `(${this.formatExpression(s.slice(2, -2))})`; - } - let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); - return "`" + r + "`"; - } - } + // slots contains sub templates defined with t-set inside t-component nodes, and + // are meant to be used by the t-slot directive. + QWeb.slots = {}; + QWeb.nextSlotId = 1; - // If a component does not define explicitely a template - // key, it needs to find a template with its name (or a parent's). This is - // qweb dependant, so we need a place to store this information indexed by - // qweb instances. - const TEMPLATE_MAP = {}; - //------------------------------------------------------------------------------ - // Component - //------------------------------------------------------------------------------ - let nextId = 1; - class Component { - //-------------------------------------------------------------------------- - // Lifecycle - //-------------------------------------------------------------------------- - /** - * Creates an instance of Component. - * - * The root component of a component tree needs an environment: - * - * ```javascript - * const root = new RootComponent(env, props); - * ``` - * - * Every other component simply needs a reference to its parent: - * - * ```javascript - * const child = new SomeComponent(parent, props); - * ``` - * - * Note that most of the time, only the root component needs to be created by - * hand. Other components should be created automatically by the framework (with - * the t-component directive in a template) - */ - constructor(parent, props) { - this.refs = {}; - const defaultProps = this.constructor.defaultProps; - if (defaultProps) { - props = this.__applyDefaultProps(props, defaultProps); - } - if (QWeb.dev) { - this.__validateProps(props || {}); - } - // is this a good idea? - // Pro: if props is empty, we can create easily a component - // Con: this is not really safe - // Pro: but creating component (by a template) is always unsafe anyway - this.props = props || {}; - let id = nextId++; - let p = null; - if (parent instanceof Component) { - p = parent; - this.env = parent.env; - parent.__owl__.children[id] = this; - } - else { - this.env = parent; - this.env.qweb.on("update", this, () => { - if (this.__owl__.isMounted) { - this.render(true); - } - if (this.__owl__.isDestroyed) { - // this is unlikely to happen, but if a root widget is destroyed, - // we want to remove our subscription. The usual way to do that - // would be to perform some check in the destroy method, but since - // it is very performance sensitive, and since this is a rare event, - // we simply do it lazily - this.env.qweb.off("update", this); - } - }); - } - this.__owl__ = { - id: id, - vnode: null, - isMounted: false, - isDestroyed: false, - parent: p, - children: {}, - cmap: {}, - renderId: 1, - renderPromise: null, - renderProps: props || null, - boundHandlers: {}, - mountedHandlers: {} - }; - } - /** - * The `el` is the root element of the component. Note that it could be null: - * this is the case if the component is not mounted yet, or is destroyed. - */ - get el() { - return this.__owl__.vnode ? this.__owl__.vnode.elm : null; - } - /** - * willStart is an asynchronous hook that can be implemented to perform some - * action before the initial rendering of a component. - * - * It will be called exactly once before the initial rendering. It is useful - * in some cases, for example, to load external assets (such as a JS library) - * before the component is rendered. - * - * Note that a slow willStart method will slow down the rendering of the user - * interface. Therefore, some effort should be made to make this method as - * fast as possible. - * - * Note: this method should not be called manually. - */ - async willStart() { } - /** - * mounted is a hook that is called each time a component is attached to the - * DOM. This is a good place to add some listeners, or to interact with the - * DOM, if the component needs to perform some measure for example. - * - * Note: this method should not be called manually. - * - * @see willUnmount - */ - mounted() { } - /** - * The willUpdateProps is an asynchronous hook, called just before new props - * are set. This is useful if the component needs some asynchronous task - * performed, depending on the props (for example, assuming that the props are - * some record Id, fetching the record data). - * - * This hook is not called during the first render (but willStart is called - * and performs a similar job). - */ - async willUpdateProps(nextProps) { } - /** - * The willPatch hook is called just before the DOM patching process starts. - * It is not called on the initial render. This is useful to get some - * information which are in the DOM. For example, the current position of the - * scrollbar - * - * The return value of willPatch will be given to the patched function. - */ - willPatch() { } - /** - * This hook is called whenever a component did actually update its props, - * state or env. - * - * This method is not called on the initial render. It is useful to interact - * with the DOM (for example, through an external library) whenever the - * component was updated. - * - * Updating the component state in this hook is possible, but not encouraged. - * One need to be careful, because updates here will cause rerender, which in - * turn will cause other calls to updated. So, we need to be particularly - * careful at avoiding endless cycles. - * - * The snapshot parameter is the result of the call to willPatch. - */ - patched(snapshot) { } - /** - * willUnmount is a hook that is called each time just before a component is - * unmounted from the DOM. This is a good place to remove some listeners, for - * example. - * - * Note: this method should not be called manually. - * - * @see mounted - */ - willUnmount() { } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - /** - * Mount the component to a target element. - * - * This should only be done if the component was created manually. Components - * created declaratively in templates are managed by the Owl system. - */ - async mount(target) { - const vnode = await this.__prepare(); - if (this.__owl__.isDestroyed) { - // component was destroyed before we get here... - return; - } - this.__patch(vnode); - target.appendChild(this.el); - if (document.body.contains(target)) { - this.__callMounted(); - } - } - unmount() { - if (this.__owl__.isMounted) { - this.__callWillUnmount(); - this.el.remove(); - } - } - async render(force = false, patchQueue) { - const __owl__ = this.__owl__; - if (!__owl__.isMounted) { - return; - } - const shouldPatch = !patchQueue; - if (shouldPatch) { - patchQueue = []; - } - const renderVDom = this.__render(force, patchQueue); - const renderId = __owl__.renderId; - await renderVDom; - if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) { - // we only update the vnode and the actual DOM if no other rendering - // occurred between now and when the render method was initially called. - this.__applyPatchQueue(patchQueue); - } - } - /** - * Destroy the component. This operation is quite complex: - * - it recursively destroy all children - * - call the willUnmount hooks if necessary - * - remove the dom node from the dom - * - * This should only be called manually if you created the component. Most - * components will be automatically destroyed. - */ - destroy() { - const __owl__ = this.__owl__; - if (!__owl__.isDestroyed) { - const el = this.el; - this.__destroy(__owl__.parent); - if (el) { - el.remove(); - } - } - } - /** - * This method is called by the component system whenever its props are - * updated. If it returns true, then the component will be rendered. - * Otherwise, it will skip the rendering (also, its props will not be updated) - */ - shouldUpdate(nextProps) { - return true; - } - /** - * This method is the correct way to update the environment of a component. Doing - * this will cause a full rerender of the component and its children, so this is - * an operation that should not be done frequently. - * - * A good usecase for updating the environment would be to update some mostly - * static config keys, such as a boolean to determine if we are in mobile - * mode or not. - */ - async updateEnv(nextEnv) { - const __owl__ = this.__owl__; - if (__owl__.parent && __owl__.parent.env === this.env) { - this.env = Object.create(this.env); - } - Object.assign(this.env, nextEnv); - if (__owl__.isMounted) { - await this.render(true); - } - } - /** - * Emit a custom event of type 'eventType' with the given 'payload' on the - * component's el, if it exists. However, note that the event will only bubble - * up to the parent DOM nodes. Thus, it must be called between mounted() and - * willUnmount(). - */ - trigger(eventType, payload) { - if (this.el) { - const ev = new CustomEvent(eventType, { - bubbles: true, - cancelable: true, - detail: payload - }); - this.el.dispatchEvent(ev); - } - } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - __destroy(parent) { - const __owl__ = this.__owl__; - const isMounted = __owl__.isMounted; - if (isMounted) { - this.willUnmount(); - __owl__.isMounted = false; - } - const children = __owl__.children; - for (let key in children) { - children[key].__destroy(this); - } - if (parent) { - let id = __owl__.id; - delete parent.__owl__.children[id]; - __owl__.parent = null; - } - __owl__.isDestroyed = true; - delete __owl__.vnode; - } - __callMounted() { - const __owl__ = this.__owl__; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (!comp.__owl__.isMounted && this.el.contains(comp.el)) { - comp.__callMounted(); - } - } - __owl__.isMounted = true; - const handlers = __owl__.mountedHandlers; - for (let key in handlers) { - handlers[key](); - } - this.mounted(); - } - __callWillUnmount() { - this.willUnmount(); - const __owl__ = this.__owl__; - __owl__.isMounted = false; - const children = __owl__.children; - for (let id in children) { - const comp = children[id]; - if (comp.__owl__.isMounted) { - comp.__callWillUnmount(); - } - } - } - async __updateProps(nextProps, forceUpdate = false, patchQueue) { - const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); - if (shouldUpdate) { - const defaultProps = this.constructor.defaultProps; - if (defaultProps) { - nextProps = this.__applyDefaultProps(nextProps, defaultProps); - } - if (QWeb.dev) { - this.__validateProps(nextProps); - } - await this.willUpdateProps(nextProps); - this.props = nextProps; - await this.render(forceUpdate, patchQueue); - } - } - __patch(vnode) { - const __owl__ = this.__owl__; - __owl__.renderPromise = null; - const target = __owl__.vnode || document.createElement(vnode.sel); - if (this.__owl__.classObj) { - vnode.data.class = Object.assign(vnode.data.class || {}, this.__owl__.classObj); - } - __owl__.vnode = patch(target, vnode); - } - __prepare() { - const __owl__ = this.__owl__; - __owl__.renderProps = this.props; - __owl__.renderPromise = this.__prepareAndRender(); - return __owl__.renderPromise; - } - async __prepareAndRender() { - await this.willStart(); - const __owl__ = this.__owl__; - if (__owl__.isDestroyed) { - return Promise.resolve(h("div")); - } - const qweb = this.env.qweb; - if (!this.template) { - let tmap = TEMPLATE_MAP[qweb.id]; - if (!tmap) { - tmap = {}; - TEMPLATE_MAP[qweb.id] = tmap; - } - let p = this.constructor; - let name = p.name; - let template = tmap[name]; - if (template) { - this.template = template; - } - else { - while ((template = p.name) && !(template in qweb.templates) && p !== Component) { - p = p.__proto__; - } - if (p === Component) { - throw new Error(`Could not find template for component "${this.constructor.name}"`); - } - else { - tmap[name] = template; - this.template = template; - } - } - } - __owl__.render = qweb.render.bind(qweb, this.template); - this.__observeState(); - return this.__render(); - } - async __render(force = false, patchQueue = []) { - const __owl__ = this.__owl__; - __owl__.renderId++; - const promises = []; - const patch = [this]; - if (__owl__.isMounted) { - patchQueue.push(patch); - } - if (__owl__.observer) { - __owl__.observer.allowMutations = false; - } - let vnode = __owl__.render(this, { - promises, - handlers: __owl__.boundHandlers, - mountedHandlers: __owl__.mountedHandlers, - forceUpdate: force, - patchQueue - }); - patch.push(vnode); - if (__owl__.observer) { - __owl__.observer.allowMutations = true; - } - // this part is critical for the patching process to be done correctly. The - // tricky part is that a child component can be rerendered on its own, which - // will update its own vnode representation without the knowledge of the - // parent component. With this, we make sure that the parent component will be - // able to patch itself properly after - vnode.key = __owl__.id; - __owl__.renderProps = this.props; - __owl__.renderPromise = Promise.all(promises).then(() => vnode); - return __owl__.renderPromise; - } - /** - * Only called by qweb t-component directive - */ - __mount(vnode, elm) { - const __owl__ = this.__owl__; - if (__owl__.classObj) { - vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj); - } - __owl__.vnode = patch(elm, vnode); - if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) { - this.__callMounted(); - } - return __owl__.vnode; - } - /** - * Only called by qweb t-component directive (when t-keepalive is set) - */ - __remount() { - const __owl__ = this.__owl__; - if (!__owl__.isMounted) { - __owl__.isMounted = true; - this.mounted(); - } - } - __observeState() { - if (this.state) { - const __owl__ = this.__owl__; - __owl__.observer = new Observer(); - __owl__.observer.observe(this.state); - __owl__.observer.notifyCB = this.render.bind(this); - } - } - /** - * Apply default props (only top level). - * - * Note that this method does not modify in place the props, it returns a new - * prop object - */ - __applyDefaultProps(props, defaultProps) { - props = props ? Object.create(props) : {}; - for (let propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - return props; - } - /** - * Apply the given patch queue. A patch is a pair [c, vn], where c is a - * Component instance and vn a VNode. - * 1) Call 'willPatch' on the component of each patch - * 2) Call '__patch' on the component of each patch - * 3) Call 'patched' on the component of each patch, in inverse order - */ - __applyPatchQueue(patchQueue) { - const patchLen = patchQueue.length; - for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch.push(patch[0].willPatch()); - } - for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch[0].__patch(patch[1]); - } - for (let i = patchLen - 1; i >= 0; i--) { - const patch = patchQueue[i]; - patch[0].patched(patch[2]); - } - } - /** - * Validate the component props (or next props) against the (static) props - * description. This is potentially an expensive operation: it may needs to - * visit recursively the props and all the children to check if they are valid. - * This is why it is only done in 'dev' mode. - */ - __validateProps(props) { - const propsDef = this.constructor.props; - if (propsDef instanceof Array) { - // list of strings (prop names) - for (let i = 0, l = propsDef.length; i < l; i++) { - if (!(propsDef[i] in props)) { - throw new Error(`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`); - } - } - } - else if (propsDef) { - // propsDef is an object now - for (let propName in propsDef) { - if (!(propName in props)) { - if (propsDef[propName] && !propsDef[propName].optional) { - throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`); - } - else { - break; - } - } - let isValid = isValidProp(props[propName], propsDef[propName]); - if (!isValid) { - throw new Error(`Props '${propName}' of invalid type in component '${this.constructor.name}'`); - } - } - } - } - } - //------------------------------------------------------------------------------ - // Prop validation helper - //------------------------------------------------------------------------------ - /** - * Check if an invidual prop value matches its (static) prop definition - */ - function isValidProp(prop, propDef) { - if (typeof propDef === "function") { - // Check if a value is constructed by some Constructor. Note that there is a - // slight abuse of language: we want to consider primitive values as well. - // - // So, even though 1 is not an instance of Number, we want to consider that - // it is valid. - if (typeof prop === "object") { - return prop instanceof propDef; - } - return typeof prop === propDef.name.toLowerCase(); - } - else if (propDef instanceof Array) { - // If this code is executed, this means that we want to check if a prop - // matches at least one of its descriptor. - let result = false; - for (let i = 0, iLen = propDef.length; i < iLen; i++) { - result = result || isValidProp(prop, propDef[i]); - } - return result; - } - // propsDef is an object - let result = isValidProp(prop, propDef.type); - if (propDef.type === Array) { - for (let i = 0, iLen = prop.length; i < iLen; i++) { - result = result && isValidProp(prop[i], propDef.element); - } - } - if (propDef.type === Object) { - const shape = propDef.shape; - for (let key in shape) { - result = result && isValidProp(prop[key], shape[key]); - } + const parser = new DOMParser(); + function htmlToVDOM(html) { + const doc = parser.parseFromString(html, "text/html"); + const result = []; + for (let child of doc.body.childNodes) { + result.push(htmlToVNode(child)); } return result; } + function htmlToVNode(node) { + if (!(node instanceof Element)) { + return { text: node.textContent }; + } + const attrs = {}; + for (let attr of node.attributes) { + attrs[attr.name] = attr.textContent; + } + const children = []; + for (let c of node.childNodes) { + children.push(htmlToVNode(c)); + } + return h(node.tagName, { attrs }, children); + } /** * Owl QWeb Directives @@ -2319,17 +2005,21 @@ //------------------------------------------------------------------------------ // t-esc and t-raw //------------------------------------------------------------------------------ - UTILS.getFragment = function (str) { + QWeb.utils.getFragment = function (str) { const temp = document.createElement("template"); temp.innerHTML = str; return temp.content; }; + QWeb.utils.htmlToVDOM = htmlToVDOM; function compileValueNode(value, node, qweb, ctx) { - if (value === "0" && ctx.caller) { - qweb._compileNode(ctx.caller, ctx); - return; + if (value === "0") { + const caller = ctx.getCaller(); + if (caller) { + qweb._compileNode(caller, ctx.getInliningContext()); + return; + } } - if (value.xml instanceof NodeList) { + if (value.xml instanceof NodeList && !value.id) { for (let node of Array.from(value.xml)) { qweb._compileNode(node, ctx); } @@ -2356,32 +2046,31 @@ ctx.rootContext.rootNode = nodeID; ctx.rootContext.parentTextNode = nodeID; ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`); + if (ctx.rootContext.shouldDefineResult) { + ctx.addLine(`result = vn${nodeID}`); + } } } else { - let fragID = ctx.generateID(); - ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); - let tempNodeID = ctx.generateID(); - ctx.addLine(`var p${tempNodeID} = {hook: {`); - ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); - ctx.addLine(`}};`); - ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); - ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); + ctx.rootContext.shouldDefineUtils = true; + ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`); } if (node.childNodes.length) { ctx.addElse(); qweb._compileChildren(node, ctx); } + if (value.xml instanceof NodeList && value.id) { + ctx.addElse(); + for (let node of Array.from(value.xml)) { + qweb._compileNode(node, ctx); + } + } ctx.closeIf(); } QWeb.addDirective({ name: "esc", priority: 70, atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } let value = ctx.getValue(node.getAttribute("t-esc")); compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); return true; @@ -2391,10 +2080,6 @@ name: "raw", priority: 80, atNodeEncounter({ node, qweb, ctx }) { - if (node.nodeName !== "t") { - let nodeID = qweb._compileGenericNode(node, ctx); - ctx = ctx.withParent(nodeID); - } let value = ctx.getValue(node.getAttribute("t-raw")); compileValueNode(value, node, qweb, ctx); return true; @@ -2410,24 +2095,22 @@ atNodeEncounter({ node, ctx }) { const variable = node.getAttribute("t-set"); let value = node.getAttribute("t-value"); + ctx.variables[variable] = ctx.variables[variable] || {}; + let qwebvar = ctx.variables[variable]; if (value) { const formattedValue = ctx.formatExpression(value); - if (ctx.variables.hasOwnProperty(variable)) { - ctx.addLine(`${ctx.variables[variable].id} = ${formattedValue}`); + if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) { + ctx.addLine(`${qwebvar.id} = ${formattedValue}`); } else { const varName = `_${ctx.generateID()}`; ctx.addLine(`var ${varName} = ${formattedValue};`); - ctx.variables[variable] = { - id: varName, - expr: formattedValue - }; + qwebvar.id = varName; + qwebvar.expr = formattedValue; } } else { - ctx.variables[variable] = { - xml: node.childNodes - }; + qwebvar.xml = node.childNodes; } return true; } @@ -2440,7 +2123,7 @@ priority: 20, atNodeEncounter({ node, ctx }) { let cond = ctx.getValue(node.getAttribute("t-if")); - ctx.addIf(`${ctx.formatExpression(cond)}`); + ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id); return false; }, finalize({ ctx }) { @@ -2452,7 +2135,7 @@ priority: 30, atNodeEncounter({ node, ctx }) { let cond = ctx.getValue(node.getAttribute("t-elif")); - ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`); + ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`); ctx.indent(); return false; }, @@ -2490,13 +2173,50 @@ const nodeCopy = node.cloneNode(true); nodeCopy.removeAttribute("t-call"); // extract variables from nodecopy - const tempCtx = new Context(); - tempCtx.nextID = ctx.rootContext.nextID; + const tempCtx = new CompilationContext(); + tempCtx.allowMultipleRoots = true; qweb._compileNode(nodeCopy, tempCtx); const vars = Object.assign({}, ctx.variables, tempCtx.variables); - ctx.rootContext.nextID = tempCtx.nextID; + const templateMap = Object.create(ctx.templates); // open new scope, if necessary const hasNewVariables = Object.keys(tempCtx.variables).length > 0; + // compile sub template + let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars)); + subCtx = subCtx.subContext("templates", templateMap); + if (templateMap[subTemplate]) { + // OUCH, IT IS A RECURSIVE TEMPLATE SITUATION... + // This is a tricky situation... We obviously cannot inline the compiled + // template. So, what we need to do is to compile it, and make sure we + // properly transfer everything from the current scope to the sub template. + ctx.rootContext.shouldTrackScope = true; + ctx.rootContext.shouldDefineOwner = true; + let subTemplateName; + if (ctx.hasParentWidget) { + subTemplateName = ctx.templateName; + } + else { + subTemplateName = `__${ctx.generateID()}`; + subCtx.variables = {}; + let id = 0; + for (let v in vars) { + subCtx.variables[v] = vars[v]; + vars[v].id = `_v${id++}`; + } + const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx); + qweb.recursiveFns[subTemplateName] = subTemplateFn; + } + let varCode = `{}`; + if (Object.keys(vars).length) { + let id = 0; + const content = Object.values(vars) + .map((v) => `_v${id++}: ${v.expr}`) + .join(","); + varCode = `{${content}}`; + } + ctx.addLine(`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`); + return true; + } + templateMap[subTemplate] = true; if (hasNewVariables) { ctx.addLine("{"); ctx.indent(); @@ -2509,8 +2229,6 @@ // todo: handle XML variables... } } - // compile sub template - const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars)); qweb._compileNode(nodeTemplate.elem, subCtx); // close new scope if (hasNewVariables) { @@ -2529,7 +2247,7 @@ priority: 10, atNodeEncounter({ node, qweb, ctx }) { ctx.rootContext.shouldProtectContext = true; - ctx = ctx.subContext("inLoop", true); + ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1); const elems = node.getAttribute("t-foreach"); const name = node.getAttribute("t-as"); let arrayID = ctx.generateID(); @@ -2543,26 +2261,19 @@ ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`); ctx.closeIf(); ctx.addLine(`var _length${keysID} = _${keysID}.length;`); - ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`); + const loopVar = `i${ctx.loopNumber}`; + ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`); ctx.indent(); - ctx.addLine(`context.${name}_first = i === 0;`); - ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`); - ctx.addLine(`context.${name}_index = i;`); - ctx.addLine(`context.${name} = _${keysID}[i];`); - ctx.addLine(`context.${name}_value = _${valuesID}[i];`); + ctx.addToScope(name + "_first", `${loopVar} === 0`); + ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`); + ctx.addToScope(name + "_index", loopVar); + ctx.addToScope(name, `_${keysID}[${loopVar}]`); + ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`); const nodeCopy = node.cloneNode(true); - let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); - if (!shouldWarn && node.tagName === "t") { - if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) { - shouldWarn = true; - } - if (!shouldWarn && - node.children.length === 1 && - node.children[0].tagName !== "t" && - !node.children[0].hasAttribute("t-key")) { - shouldWarn = true; - } - } + let shouldWarn = !nodeCopy.hasAttribute("t-key") && + node.children.length === 1 && + node.children[0].tagName !== "t" && + !node.children[0].hasAttribute("t-key"); if (shouldWarn) { console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`); } @@ -2604,7 +2315,6 @@ * - t-on * - t-ref * - t-transition - * - t-component/t-keepalive * - t-mounted * - t-slot * - t-model @@ -2633,30 +2343,33 @@ extraArgs = args.slice(1, -1); return ""; }); - ctx.addIf(`!context['${handlerName}']`); - ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`); - ctx.closeIf(); let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner"; - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function (mod) { - return MODS_CODE[mod]; - }) - .join(""); - handler += `context['${handlerName}'].call(${params}, e);}`; + let handler = `function (e) {`; + handler += mods + .map(function (mod) { + return MODS_CODE[mod]; + }) + .join(""); + if (handlerName) { + if (!extraArgs) { + handler += `const fn = context['${handlerName}'];`; + handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`; + handler += `}`; + ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`); + ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); + } + else { + const handlerKey = `handler${ctx.generateID()}`; + ctx.addLine(`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`); + handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`; + handler += `}`; + ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); + } } else { - handler = `context['${handlerName}'].bind(${params})`; - } - if (extraArgs) { + handler += "}"; ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); } - else { - ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } } }); //------------------------------------------------------------------------------ @@ -2666,18 +2379,20 @@ name: "ref", priority: 95, atNodeCreation({ ctx, value, addNodeHook }) { + ctx.rootContext.shouldDefineRefs = true; const refKey = `ref${ctx.generateID()}`; ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); - addNodeHook("create", `context.refs[${refKey}] = n.elm;`); + addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`); + addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`); } }); //------------------------------------------------------------------------------ // t-transition //------------------------------------------------------------------------------ - UTILS.nextFrame = function (cb) { + QWeb.utils.nextFrame = function (cb) { requestAnimationFrame(() => requestAnimationFrame(cb)); }; - UTILS.transitionInsert = function (vn, name) { + QWeb.utils.transitionInsert = function (vn, name) { const elm = vn.elm; // remove potential duplicated vnode that is currently being removed, to // prevent from having twice the same node in the DOM during an animation @@ -2687,6 +2402,8 @@ } elm.classList.add(name + "-enter"); elm.classList.add(name + "-enter-active"); + elm.classList.remove(name + "-leave-active"); + elm.classList.remove(name + "-leave-to"); const finalize = () => { elm.classList.remove(name + "-enter-active"); elm.classList.remove(name + "-enter-to"); @@ -2697,12 +2414,15 @@ whenTransitionEnd(elm, finalize); }); }; - UTILS.transitionRemove = function (vn, name, rm) { + QWeb.utils.transitionRemove = function (vn, name, rm) { const elm = vn.elm; elm.setAttribute("data-owl-key", vn.key); elm.classList.add(name + "-leave"); elm.classList.add(name + "-leave-active"); const finalize = () => { + if (!elm.classList.contains(name + "-leave-active")) { + return; + } elm.classList.remove(name + "-leave-active"); elm.classList.remove(name + "-leave-to"); rm(); @@ -2744,23 +2464,158 @@ QWeb.addDirective({ name: "transition", priority: 96, - atNodeCreation({ value, addNodeHook }) { + atNodeCreation({ ctx, value, addNodeHook }) { + ctx.rootContext.shouldDefineUtils = true; let name = value; const hooks = { - insert: `this.utils.transitionInsert(vn, '${name}');`, - remove: `this.utils.transitionRemove(vn, '${name}', rm);` + insert: `utils.transitionInsert(vn, '${name}');`, + remove: `utils.transitionRemove(vn, '${name}', rm);` }; for (let hookName in hooks) { addNodeHook(hookName, hooks[hookName]); } } }); + //------------------------------------------------------------------------------ + // t-slot + //------------------------------------------------------------------------------ + QWeb.addDirective({ + name: "slot", + priority: 80, + atNodeEncounter({ ctx, value }) { + const slotKey = ctx.generateID(); + ctx.rootContext.shouldDefineOwner = true; + ctx.addLine(`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`); + ctx.addIf(`slot${slotKey}`); + let parentNode = `c${ctx.parentNode}`; + if (!ctx.parentNode) { + ctx.rootContext.shouldDefineResult = true; + ctx.rootContext.shouldDefineUtils = true; + parentNode = `children${ctx.generateID()}`; + ctx.addLine(`let ${parentNode}= []`); + ctx.addLine(`result = {}`); + } + ctx.addLine(`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`); + if (!ctx.parentNode) { + ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`); + } + ctx.closeIf(); + return true; + } + }); + //------------------------------------------------------------------------------ + // t-model + //------------------------------------------------------------------------------ + QWeb.utils.toNumber = function (val) { + const n = parseFloat(val); + return isNaN(n) ? val : n; + }; + QWeb.addDirective({ + name: "model", + priority: 42, + atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) { + const type = node.getAttribute("type"); + let handler; + let event = fullName.includes(".lazy") ? "change" : "input"; + // we keep here a reference to the "base expression" (if the expression + // is `t-model="some.expr.value", then the base expression is "some.expr"). + // This is necessary so we can capture it in the handler closure. + let expr = ctx.formatExpression(value); + const index = expr.lastIndexOf("."); + const baseExpr = expr.slice(0, index); + ctx.addLine(`let expr${nodeID} = ${baseExpr};`); + expr = `expr${nodeID}.${expr.slice(index + 1)}`; + const key = ctx.generateTemplateKey(); + if (node.tagName === "select") { + ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); + addNodeHook("create", `n.elm.value=${expr};`); + event = "change"; + handler = `(ev) => {${expr} = ev.target.value}`; + } + else if (type === "checkbox") { + ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`); + handler = `(ev) => {${expr} = ev.target.checked}`; + } + else if (type === "radio") { + const nodeValue = node.getAttribute("value"); + ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`); + handler = `(ev) => {${expr} = ev.target.value}`; + event = "click"; + } + else { + ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); + const trimCode = fullName.includes(".trim") ? ".trim()" : ""; + let valueCode = `ev.target.value${trimCode}`; + if (fullName.includes(".number")) { + ctx.rootContext.shouldDefineUtils = true; + valueCode = `utils.toNumber(${valueCode})`; + } + handler = `(ev) => {${expr} = ${valueCode}}`; + } + ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`); + ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`); + } + }); + //------------------------------------------------------------------------------ + // t-key + //------------------------------------------------------------------------------ + QWeb.addDirective({ + name: "key", + priority: 45, + atNodeEncounter({ ctx, value }) { + let id = ctx.generateID(); + ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`); + ctx.currentKey = `nodeKey${id}`; + } + }); + + const config = {}; + Object.defineProperty(config, "mode", { + get() { + return QWeb.dev ? "dev" : "prod"; + }, + set(mode) { + QWeb.dev = mode === "dev"; + if (QWeb.dev) { + const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`; + console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`); + } + else { + console.log(`Owl is now running in 'prod' mode.`); + } + } + }); + + /** + * We define here OwlEvent, a subclass of CustomEvent, with an additional + * attribute: + * - originalComponent: the component that triggered the event + */ + class OwlEvent extends CustomEvent { + constructor(component, eventType, options) { + super(eventType, options); + this.originalComponent = component; + } + } + //------------------------------------------------------------------------------ // t-component //------------------------------------------------------------------------------ const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, { self: "if (e.target !== vn.elm) {return}" }); + QWeb.utils.defineProxy = function defineProxy(target, source) { + for (let k in source) { + Object.defineProperty(target, k, { + get() { + return source[k]; + }, + set(val) { + source[k] = val; + } + }); + } + }; /** * The t-component directive is certainly a complicated and hard to maintain piece * of code. To help you, fellow developer, if you have to maintain it, I offer @@ -2923,15 +2778,15 @@ */ QWeb.addDirective({ name: "component", - extraNames: ["props", "keepalive", "asyncroot"], + extraNames: ["props"], priority: 100, atNodeEncounter({ ctx, value, node, qweb }) { ctx.addLine("//COMPONENT"); ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineQWeb = true; + ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineUtils = true; - let keepAlive = node.getAttribute("t-keepalive") ? true : false; - let async = node.getAttribute("t-asyncroot") ? true : false; + let hasDynamicProps = node.getAttribute("t-props") ? true : false; // t-on- events and t-transition const events = []; let transition = ""; @@ -2943,11 +2798,11 @@ if (name.startsWith("t-on-")) { const [eventName, ...mods] = name.slice(5).split("."); let extraArgs; - let handlerName = value.replace(/\(.*\)/, function (args) { + let handlerValue = value.replace(/\(.*\)/, function (args) { extraArgs = args.slice(1, -1); return ""; }); - events.push([eventName, mods, handlerName, extraArgs]); + events.push([eventName, mods, handlerValue, extraArgs]); } else if (name === "t-transition") { transition = value; @@ -2959,60 +2814,30 @@ } } } - let key = node.getAttribute("t-key"); - if (key) { - key = ctx.formatExpression(key); - } // computing the props string representing the props object let propStr = Object.keys(props) .map(k => k + ":" + props[k]) .join(","); - let dummyID = ctx.generateID(); - let defID = ctx.generateID(); let componentID = ctx.generateID(); - let keyID = key && ctx.generateID(); - if (key) { - // we bind a variable to the key (could be a complex expression, so we - // want to evaluate it only once) - ctx.addLine(`let key${keyID} = ${key};`); - } - ctx.addLine(`let def${defID};`); - let templateID = key - ? `key${keyID}` - : ctx.inLoop - ? `String(-${componentID} - i)` - : String(componentID); - if (ctx.allowMultipleRoots) { - // necessary to prevent collisions - if (!key && ctx.inLoop) { - let id = ctx.generateID(); - ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`); - templateID = `template${id}`; - } - else { - templateID = `"_slot_${templateID}"`; - } - } + const templateKey = ctx.generateTemplateKey(); let ref = node.getAttribute("t-ref"); let refExpr = ""; let refKey = ""; if (ref) { + ctx.rootContext.shouldDefineRefs = true; refKey = `ref${ctx.generateID()}`; ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); - refExpr = `context.refs[${refKey}] = w${componentID};`; + refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`; } - let transitionsInsertCode = ""; - if (transition) { - transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`; - } - let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; - if (ref && !keepAlive) { - finalizeComponentCode += `delete context.refs[${refKey}];`; + let finalizeComponentCode = `w${componentID}.destroy();`; + if (ref) { + finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`; } if (transition) { finalizeComponentCode = `let finalize = () => { ${finalizeComponentCode} }; + delete w${componentID}.__owl__.transitionInserted; utils.transitionRemove(vn, '${transition}', finalize);`; } let createHook = ""; @@ -3039,7 +2864,7 @@ if (tattClass) { let tattExpr = ctx.formatExpression(tattClass); if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") { - tattExpr = `this.utils.toObj(${tattExpr})`; + tattExpr = `utils.toObj(${tattExpr})`; } if (classAttr) { ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`); @@ -3050,10 +2875,10 @@ } } let eventsCode = events - .map(function ([eventName, mods, handlerName, extraArgs]) { + .map(function ([eventName, mods, handlerValue, extraArgs]) { let params = "owner"; if (extraArgs) { - if (ctx.inLoop) { + if (ctx.loopNumber) { let argId = ctx.generateID(); // we need to evaluate the arguments now, because the handler will // be set asynchronously later when the widget is ready, and the @@ -3065,19 +2890,17 @@ params = `owner, ${ctx.formatExpression(extraArgs)}`; } } - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function (mod) { - return T_COMPONENT_MODS_CODE[mod]; - }) - .join(""); - handler += `owner['${handlerName}'].call(${params}, e);}`; - } - else { - handler = `owner['${handlerName}'].bind(${params})`; + let handler = `function (e) {`; + handler += mods + .map(function (mod) { + return T_COMPONENT_MODS_CODE[mod]; + }) + .join(""); + if (handlerValue) { + handler += `const fn = owner['${handlerValue}'];`; + handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`; } + handler += `}`; return `vn.elm.addEventListener('${eventName}', ${handler});`; }) .join(""); @@ -3085,37 +2908,87 @@ const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : ""; createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`; } - ctx.addLine(`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); - if (async) { - ctx.addLine(`const patchQueue${componentID} = [];`); - ctx.addLine(`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`); + ctx.addLine(`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`); + let shouldProxy = !ctx.parentNode; + if (shouldProxy) { + let id = ctx.generateID(); + ctx.rootContext.rootNode = id; + shouldProxy = true; + ctx.rootContext.shouldDefineResult = true; + ctx.addLine(`let vn${id} = {};`); + ctx.addLine(`result = vn${id};`); + } + if (hasDynamicProps) { + const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")); + ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`); } else { - ctx.addLine(`c${ctx.parentNode}.push(null);`); + ctx.addLine(`let props${componentID} = {${propStr}};`); } - ctx.addLine(`let props${componentID} = {${propStr}};`); - ctx.addIf(`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`); - ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`); - ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`); - ctx.addElse(); + ctx.addIf(`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`); ctx.addLine(`w${componentID}.destroy();`); ctx.addLine(`w${componentID} = false;`); ctx.closeIf(); - ctx.closeIf(); - ctx.addIf(`!w${componentID}`); + let registerCode = ""; + if (shouldProxy) { + registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`; + } + // SLOTS + const varDefs = []; + const hasSlots = node.childNodes.length; + if (hasSlots) { + ctx.rootContext.shouldTrackScope = true; + for (let v of Object.values(ctx.variables)) { + if (v["id"]) { + varDefs.push(v["id"]); + } + } + } + let scopeVars; + if (hasSlots) { + let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`; + let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined"; + scopeVars = `${scope}, ${vars}`; + } + else { + scopeVars = "undefined, undefined"; + } + ctx.addIf(`w${componentID}`); + // need to update component + let styleCode = ""; + if (tattStyle) { + styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`; + } + ctx.addLine(`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars && + ", " + scopeVars})${styleCode};`); + ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`); + if (registerCode) { + ctx.addLine(registerCode); + } + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push(pvnode);`); + } + ctx.addElse(); // new component - ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`); - ctx.addLine(`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`); + let dynamicFallback = ""; + if (!value.match(INTERP_REGEXP)) { + dynamicFallback = `|| ${ctx.formatExpression(value)}`; + } + const interpValue = ctx.interpolate(value); + ctx.addLine(`let componentKey${componentID} = ${interpValue};`); + ctx.addLine(`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`); // maybe only do this in dev mode... ctx.addLine(`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`); - ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`); - ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`); - // SLOTS - if (node.childNodes.length) { + ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`); + if (transition) { + ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`); + ctx.addLine(`w${componentID}.__patch = fiber => {__patch${componentID}.call(w${componentID}, fiber); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`); + } + ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`); + if (hasSlots) { const clone = node.cloneNode(true); const slotNodes = clone.querySelectorAll("[t-set]"); - const slotId = qweb.nextSlotId++; + const slotId = QWeb.nextSlotId++; ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`); if (slotNodes.length) { for (let i = 0, length = slotNodes.length; i < length; i++) { @@ -3123,8 +2996,8 @@ slotNode.parentElement.removeChild(slotNode); const key = slotNode.getAttribute("t-set"); slotNode.removeAttribute("t-set"); - const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode); - qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb); + const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); + QWeb.slots[`${slotId}_${key}`] = slotFn; } } if (clone.childNodes.length) { @@ -3132,450 +3005,1822 @@ for (let child of Object.values(clone.childNodes)) { t.appendChild(child); } - const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode); - qweb.slots[`${slotId}_default`] = slotFn.bind(qweb); + const slotFn = qweb._compile(`slot_default_template`, t, ctx); + QWeb.slots[`${slotId}_default`] = slotFn; } } - ctx.addLine(`def${defID} = w${componentID}.__prepare();`); + ctx.addLine(`let fiber = w${componentID}.__prepare(extra.fiber, ${scopeVars}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`); // hack: specify empty remove hook to prevent the node from being removed from the DOM - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`); - ctx.addElse(); - // need to update component - const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; - ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`); - let keepAliveCode = ""; - if (keepAlive) { - keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`; + const insertHook = refExpr ? `insert(vn) {${refExpr}},` : ""; + ctx.addLine(`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`); + if (registerCode) { + ctx.addLine(registerCode); } - ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`); + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push(pvnode);`); + } + ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`); ctx.closeIf(); if (classObj) { ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`); } - if (async) { - ctx.addLine(`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`); - } - else { - ctx.addLine(`extra.promises.push(def${defID});`); - } - if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) { - ctx.closeIf(); - } + ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`); return true; } }); - //------------------------------------------------------------------------------ - // t-mounted - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "mounted", - priority: 97, - atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) { - ctx.rootContext.shouldDefineOwner = true; - const eventName = fullName.slice(5); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handler = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`; - if (extraArgs) { - ctx.addLine(`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`); - } - else { - ctx.addLine(`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`); - } - addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`); - } - }); - //------------------------------------------------------------------------------ - // t-slot - //------------------------------------------------------------------------------ - QWeb.addDirective({ - name: "slot", - priority: 80, - atNodeEncounter({ ctx, value }) { - const slotKey = ctx.generateID(); - ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); - ctx.addIf(`slot${slotKey}`); - ctx.addLine(`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`); - ctx.closeIf(); - return true; - } - }); - //------------------------------------------------------------------------------ - // t-model - //------------------------------------------------------------------------------ - UTILS.toNumber = function (val) { - const n = parseFloat(val); - return isNaN(n) ? val : n; - }; - QWeb.addDirective({ - name: "model", - priority: 42, - atNodeCreation({ ctx, nodeID, value, node, fullName }) { - const type = node.getAttribute("type"); - let handler; - let event = fullName.includes(".lazy") ? "change" : "input"; - if (node.tagName === "select") { - ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); - event = "change"; - handler = `(ev) => {context.state['${value}'] = ev.target.value}`; - } - else if (type === "checkbox") { - ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`); - handler = `(ev) => {context.state['${value}'] = ev.target.checked}`; - } - else if (type === "radio") { - const nodeValue = node.getAttribute("value"); - ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`); - handler = `(ev) => {context.state['${value}'] = ev.target.value}`; - event = "click"; - } - else { - ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); - const trimCode = fullName.includes(".trim") ? ".trim()" : ""; - let valueCode = `ev.target.value${trimCode}`; - if (fullName.includes(".number")) { - ctx.rootContext.shouldDefineUtils = true; - valueCode = `utils.toNumber(${valueCode})`; - } - handler = `(ev) => {context.state['${value}'] = ${valueCode}}`; - } - ctx.addLine(`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`); - ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`); - } - }); - class Store extends EventBus { - constructor(config, options = {}) { - super(); - this._commitLevel = 0; - this.history = []; - this._updateId = 1; - this.debug = options.debug || false; - this.state = config.state || {}; - this.actions = config.actions; - this.mutations = config.mutations; - this.env = config.env; - this.observer = new Observer(); - this.observer.notifyCB = this.__notifyComponents.bind(this); - this.observer.allowMutations = false; - this.observer.observe(this.state); - this.getters = {}; - this._gettersCache = {}; - if (this.debug) { - this.history.push({ state: this.state }); - } - const cTypes = ["undefined", "number", "string"]; - for (let entry of Object.entries(config.getters || {})) { - const name = entry[0]; - const func = entry[1]; - this.getters[name] = payload => { - if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) { - this._gettersCache[name] = this._gettersCache[name] || {}; - this._gettersCache[name][payload] = - this._gettersCache[name][payload] || - func({ state: this.state, getters: this.getters }, payload); - return this._gettersCache[name][payload]; + class Scheduler { + constructor(requestAnimationFrame) { + this.tasks = []; + this.isRunning = false; + this.requestAnimationFrame = requestAnimationFrame; + } + start() { + this.isRunning = true; + this.scheduleTasks(); + } + stop() { + this.isRunning = false; + } + addFiber(fiber) { + // if the fiber was remapped into a larger rendering fiber, it may not be a + // root fiber. But we only want to register root fibers + fiber = fiber.root; + return new Promise((resolve, reject) => { + if (fiber.error) { + return reject(fiber.error); + } + this.tasks.push({ + fiber, + callback: () => { + if (fiber.error) { + return reject(fiber.error); + } + resolve(); } - return func({ state: this.state, getters: this.getters }, payload); + }); + if (!this.isRunning) { + this.start(); + } + }); + } + /** + * Process all current tasks. This only applies to the fibers that are ready. + * Other tasks are left unchanged. + */ + flush() { + let tasks = this.tasks; + this.tasks = []; + tasks = tasks.filter(task => { + if (task.fiber.isCompleted) { + task.callback(); + return false; + } + if (task.fiber.counter === 0) { + if (!task.fiber.error) { + try { + task.fiber.complete(); + } + catch (e) { + task.fiber.handleError(e); + } + } + task.callback(); + return false; + } + return true; + }); + this.tasks = tasks.concat(this.tasks); + if (this.tasks.length === 0) { + this.stop(); + } + } + scheduleTasks() { + this.requestAnimationFrame(() => { + this.flush(); + if (this.isRunning) { + this.scheduleTasks(); + } + }); + } + } + const raf = window.requestAnimationFrame.bind(window); + const scheduler = new Scheduler(raf); + + /** + * Owl Fiber Class + * + * Fibers are small abstractions designed to contain all the internal state + * associated with a "rendering work unit", relative to a specific component. + * + * A rendering will cause the creation of a fiber for each impacted components. + * + * Fibers capture all that necessary information, which is critical to owl + * asynchronous rendering pipeline. Fibers can be cancelled, can be in different + * states and in general determine the state of the rendering. + */ + class Fiber { + constructor(parent, component, force, target) { + this.id = Fiber.nextId++; + // isCompleted means that the rendering corresponding to this fiber's work is + // done, either because the component has been mounted or patched, or because + // fiber has been cancelled. + this.isCompleted = false; + // the fibers corresponding to component updates (updateProps) need to call + // the willPatch and patched hooks from the corresponding component. However, + // fibers corresponding to a new component do not need to do that. So, the + // shouldPatch hook is the boolean that we check whenever we need to apply + // a patch. + this.shouldPatch = true; + // isRendered is the last state of a fiber. If true, this means that it has + // been rendered and is inert (so, it should not be taken into account when + // counting the number of active fibers). + this.isRendered = false; + // the counter number is a critical information. It is only necessary for a + // root fiber. For that fiber, this number counts the number of active sub + // fibers. When that number reaches 0, the fiber can be applied by the + // scheduler. + this.counter = 0; + this.vnode = null; + this.child = null; + this.sibling = null; + this.lastChild = null; + this.parent = null; + this.component = component; + this.force = force; + this.target = target; + const __owl__ = component.__owl__; + this.scope = __owl__.scope; + this.vars = __owl__.vars; + this.root = parent ? parent.root : this; + this.parent = parent; + let oldFiber = __owl__.currentFiber; + if (oldFiber && !oldFiber.isCompleted) { + if (oldFiber.root === oldFiber && !parent) { + // both oldFiber and this fiber are root fibers + this._reuseFiber(oldFiber); + return oldFiber; + } + else { + this._remapFiber(oldFiber); + } + } + this.root.counter++; + __owl__.currentFiber = this; + } + /** + * When the oldFiber is not completed yet, and both oldFiber and this fiber + * are root fibers, we want to reuse the oldFiber instead of creating a new + * one. Doing so will guarantee that the initiator(s) of those renderings will + * be notified (the promise will resolve) when the last rendering will be done. + * + * This function thus assumes that oldFiber is a root fiber. + */ + _reuseFiber(oldFiber) { + oldFiber.cancel(); // cancel children fibers + oldFiber.isCompleted = false; // keep the root fiber alive + oldFiber.isRendered = false; // the fiber has to be re-rendered + if (oldFiber.child) { + // remove relation to children + oldFiber.child.parent = null; + oldFiber.child = null; + oldFiber.lastChild = null; + } + oldFiber.counter = 1; // re-initialize counter + oldFiber.id = Fiber.nextId++; + } + /** + * In some cases, a rendering initiated at some component can detect that it + * should be part of a larger rendering initiated somewhere up the component + * tree. In that case, it needs to cancel the previous rendering and + * remap itself as a part of the current parent rendering. + */ + _remapFiber(oldFiber) { + oldFiber.cancel(); + this.shouldPatch = oldFiber.shouldPatch; + if (oldFiber === oldFiber.root) { + oldFiber.counter++; + } + if (oldFiber.parent && !this.parent) { + // re-map links + this.parent = oldFiber.parent; + this.root = this.parent.root; + this.sibling = oldFiber.sibling; + if (this.parent.lastChild === oldFiber) { + this.parent.lastChild = this; + } + if (this.parent.child === oldFiber) { + this.parent.child = this; + } + else { + let current = this.parent.child; + while (true) { + if (current.sibling === oldFiber) { + current.sibling = this; + break; + } + current = current.sibling; + } + } + } + } + /** + * This function has been taken from + * https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7 + */ + _walk(doWork) { + let root = this; + let current = this; + while (true) { + const child = doWork(current); + if (child) { + current = child; + continue; + } + if (current === root) { + return; + } + while (!current.sibling) { + if (!current.parent || current.parent === root) { + return; + } + current = current.parent; + } + current = current.sibling; + } + } + /** + * Successfully complete the work of the fiber: call the mount or patch hooks + * and patch the DOM. This function is called once the fiber and its children + * are ready, and the scheduler decides to process it. + */ + complete() { + let component = this.component; + this.isCompleted = true; + if (!this.target && !component.__owl__.isMounted) { + return; + } + // build patchQueue + const patchQueue = []; + const doWork = function (f) { + patchQueue.push(f); + return f.child; + }; + this._walk(doWork); + const patchLen = patchQueue.length; + // call willPatch hook on each fiber of patchQueue + for (let i = 0; i < patchLen; i++) { + const fiber = patchQueue[i]; + if (fiber.shouldPatch) { + component = fiber.component; + if (component.__owl__.willPatchCB) { + component.__owl__.willPatchCB(); + } + component.willPatch(); + } + } + // call __patch on each fiber of (reversed) patchQueue + for (let i = patchLen - 1; i >= 0; i--) { + const fiber = patchQueue[i]; + component = fiber.component; + component.__patch(fiber.vnode); + if (!fiber.shouldPatch && (!fiber.target || i !== 0)) { + component.__owl__.pvnode.elm = component.__owl__.vnode.elm; + } + component.__owl__.currentFiber = null; + } + // insert into the DOM (mount case) + let inDOM = false; + if (this.target) { + this.target.appendChild(this.component.el); + inDOM = document.body.contains(this.target); + this.component.env.qweb.trigger("dom-appended"); + } + // call patched/mounted hook on each fiber of (reversed) patchQueue + for (let i = patchLen - 1; i >= 0; i--) { + const fiber = patchQueue[i]; + component = fiber.component; + if (fiber.shouldPatch && !this.target) { + component.patched(); + if (component.__owl__.patchedCB) { + component.__owl__.patchedCB(); + } + } + else if (this.target ? inDOM : true) { + component.__callMounted(); + } + } + } + /** + * Cancel a fiber and all its children. + */ + cancel() { + this._walk(f => { + if (!f.isRendered) { + f.root.counter--; + } + f.isCompleted = true; + return f.child; + }); + } + /** + * This is the global error handler for errors occurring in Owl main lifecycle + * methods. Caught errors are triggered on the QWeb instance, and are + * potentially given to some parent component which implements `catchError`. + * + * If there are no such component, we destroy everything. This is better than + * being in a corrupted state. + */ + handleError(error) { + let component = this.component; + this.vnode = component.__owl__.vnode || h("div"); + const qweb = component.env.qweb; + let root = component; + let canCatch = false; + while (component && !(canCatch = !!component.catchError)) { + root = component; + component = component.__owl__.parent; + } + qweb.trigger("error", error); + if (canCatch) { + component.catchError(error); + } + else { + // the 3 next lines aim to mark the root fiber as being in error, and + // to force it to end, without waiting for its children + this.root.counter = 0; + this.root.error = error; + scheduler.flush(); + root.destroy(); + } + } + } + Fiber.nextId = 1; + + //------------------------------------------------------------------------------ + // Prop validation helper + //------------------------------------------------------------------------------ + /** + * Validate the component props (or next props) against the (static) props + * description. This is potentially an expensive operation: it may needs to + * visit recursively the props and all the children to check if they are valid. + * This is why it is only done in 'dev' mode. + */ + QWeb.utils.validateProps = function (Widget, props) { + const propsDef = Widget.props; + if (propsDef instanceof Array) { + // list of strings (prop names) + for (let i = 0, l = propsDef.length; i < l; i++) { + const propName = propsDef[i]; + if (propName[propName.length - 1] === "?") { + // optional prop + break; + } + if (!(propName in props)) { + throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`); + } + } + for (let key in props) { + if (!propsDef.includes(key) && !propsDef.includes(key + "?")) { + throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`); + } + } + } + else if (propsDef) { + // propsDef is an object now + for (let propName in propsDef) { + if (props[propName] === undefined) { + if (propsDef[propName] && !propsDef[propName].optional) { + throw new Error(`Missing props '${propName}' (component '${Widget.name}')`); + } + else { + break; + } + } + let isValid; + try { + isValid = isValidProp(props[propName], propsDef[propName]); + } + catch (e) { + e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`; + throw e; + } + if (!isValid) { + throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`); + } + } + for (let propName in props) { + if (!(propName in propsDef)) { + throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`); + } + } + } + }; + /** + * Check if an invidual prop value matches its (static) prop definition + */ + function isValidProp(prop, propDef) { + if (propDef === true) { + return true; + } + if (typeof propDef === "function") { + // Check if a value is constructed by some Constructor. Note that there is a + // slight abuse of language: we want to consider primitive values as well. + // + // So, even though 1 is not an instance of Number, we want to consider that + // it is valid. + if (typeof prop === "object") { + return prop instanceof propDef; + } + return typeof prop === propDef.name.toLowerCase(); + } + else if (propDef instanceof Array) { + // If this code is executed, this means that we want to check if a prop + // matches at least one of its descriptor. + let result = false; + for (let i = 0, iLen = propDef.length; i < iLen; i++) { + result = result || isValidProp(prop, propDef[i]); + } + return result; + } + // propsDef is an object + if (propDef.optional && prop === undefined) { + return true; + } + let result = propDef.type ? isValidProp(prop, propDef.type) : true; + if (propDef.validate) { + result = result && propDef.validate(prop); + } + if (propDef.type === Array && propDef.element) { + for (let i = 0, iLen = prop.length; i < iLen; i++) { + result = result && isValidProp(prop[i], propDef.element); + } + } + if (propDef.type === Object && propDef.shape) { + const shape = propDef.shape; + for (let key in shape) { + result = result && isValidProp(prop[key], shape[key]); + } + if (result) { + for (let propName in prop) { + if (!(propName in shape)) { + throw new Error(`unknown prop '${propName}'`); + } + } + } + } + return result; + } + + const portalSymbol = Symbol("portal"); // FIXME + //------------------------------------------------------------------------------ + // Component + //------------------------------------------------------------------------------ + let nextId = 1; + class Component { + //-------------------------------------------------------------------------- + // Lifecycle + //-------------------------------------------------------------------------- + /** + * Creates an instance of Component. + * + * Note that most of the time, only the root component needs to be created by + * hand. Other components should be created automatically by the framework (with + * the t-component directive in a template) + */ + constructor(parent, props) { + Component.current = this; + let constr = this.constructor; + const defaultProps = constr.defaultProps; + if (defaultProps) { + props = props || {}; + this.__applyDefaultProps(props, defaultProps); + } + this.props = props; + if (QWeb.dev) { + QWeb.utils.validateProps(constr, this.props); + } + const id = nextId++; + let depth; + if (parent) { + this.env = parent.env; + const __powl__ = parent.__owl__; + __powl__.children[id] = this; + depth = __powl__.depth + 1; + } + else { + // we are the root component + this.env = this.constructor.env; + if (!this.env.qweb) { + this.env.qweb = new QWeb(); + } + this.env.qweb.on("update", this, () => { + if (this.__owl__.isMounted) { + this.render(true); + } + if (this.__owl__.isDestroyed) { + // this is unlikely to happen, but if a root widget is destroyed, + // we want to remove our subscription. The usual way to do that + // would be to perform some check in the destroy method, but since + // it is very performance sensitive, and since this is a rare event, + // we simply do it lazily + this.env.qweb.off("update", this); + } + }); + depth = 0; + } + const qweb = this.env.qweb; + const template = constr.template || this.__getTemplate(qweb); + this.__owl__ = { + id: id, + depth: depth, + vnode: null, + pvnode: null, + isMounted: false, + isDestroyed: false, + parent: parent || null, + children: {}, + cmap: {}, + currentFiber: null, + parentLastFiberId: 0, + boundHandlers: {}, + mountedCB: null, + willUnmountCB: null, + willPatchCB: null, + patchedCB: null, + willStartCB: null, + willUpdatePropsCB: null, + observer: null, + renderFn: qweb.render.bind(qweb, template), + classObj: null, + refs: null, + scope: null, + vars: null + }; + } + /** + * The `el` is the root element of the component. Note that it could be null: + * this is the case if the component is not mounted yet, or is destroyed. + */ + get el() { + return this.__owl__.vnode ? this.__owl__.vnode.elm : null; + } + /** + * willStart is an asynchronous hook that can be implemented to perform some + * action before the initial rendering of a component. + * + * It will be called exactly once before the initial rendering. It is useful + * in some cases, for example, to load external assets (such as a JS library) + * before the component is rendered. + * + * Note that a slow willStart method will slow down the rendering of the user + * interface. Therefore, some effort should be made to make this method as + * fast as possible. + * + * Note: this method should not be called manually. + */ + async willStart() { } + /** + * mounted is a hook that is called each time a component is attached to the + * DOM. This is a good place to add some listeners, or to interact with the + * DOM, if the component needs to perform some measure for example. + * + * Note: this method should not be called manually. + * + * @see willUnmount + */ + mounted() { } + /** + * The willUpdateProps is an asynchronous hook, called just before new props + * are set. This is useful if the component needs some asynchronous task + * performed, depending on the props (for example, assuming that the props are + * some record Id, fetching the record data). + * + * This hook is not called during the first render (but willStart is called + * and performs a similar job). + */ + async willUpdateProps(nextProps) { } + /** + * The willPatch hook is called just before the DOM patching process starts. + * It is not called on the initial render. This is useful to get some + * information which are in the DOM. For example, the current position of the + * scrollbar + */ + willPatch() { } + /** + * This hook is called whenever a component did actually update its props, + * state or env. + * + * This method is not called on the initial render. It is useful to interact + * with the DOM (for example, through an external library) whenever the + * component was updated. + * + * Updating the component state in this hook is possible, but not encouraged. + * One need to be careful, because updates here will cause rerender, which in + * turn will cause other calls to updated. So, we need to be particularly + * careful at avoiding endless cycles. + */ + patched() { } + /** + * willUnmount is a hook that is called each time just before a component is + * unmounted from the DOM. This is a good place to remove some listeners, for + * example. + * + * Note: this method should not be called manually. + * + * @see mounted + */ + willUnmount() { } + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + /** + * Mount the component to a target element. + * + * This should only be done if the component was created manually. Components + * created declaratively in templates are managed by the Owl system. + * + * Note that a component can be mounted an unmounted several times + */ + async mount(target) { + const __owl__ = this.__owl__; + if (__owl__.isMounted) { + return Promise.resolve(); + } + if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) { + let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`; + message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`; + throw new Error(message); + } + const fiber = new Fiber(null, this, false, target); + fiber.shouldPatch = false; + if (!__owl__.vnode) { + this.__prepareAndRender(fiber, () => { }); + } + else { + this.__render(fiber); + } + return scheduler.addFiber(fiber); + } + /** + * The unmount method is the opposite of the mount method. It is useful + * to call willUnmount calls and remove the component from the DOM. + */ + unmount() { + if (this.__owl__.isMounted) { + this.__callWillUnmount(); + this.el.remove(); + } + } + /** + * The render method is the main entry point to render a component (once it + * is ready. This method is not initially called when the component is + * rendered the first time). + * + * This method will cause all its sub components to potentially rerender + * themselves. Note that `render` is not called if a component is updated via + * its props. + */ + async render(force = false) { + const __owl__ = this.__owl__; + if (!__owl__.isMounted && !__owl__.currentFiber) { + // if we get here, this means that the component was either never mounted, + // or was unmounted and some state change triggered a render. Either way, + // we do not want to actually render anything in this case. + return; + } + if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) { + return scheduler.addFiber(__owl__.currentFiber.root); + } + // if we aren't mounted at this point, it implies that there is a + // currentFiber that is already rendered (isRendered is true), so we are + // about to be mounted + const isMounted = __owl__.isMounted; + const fiber = new Fiber(null, this, force, null); + Promise.resolve().then(() => { + if (__owl__.isMounted || !isMounted) { + // we are mounted (__owl__.isMounted), or if we are currently being + // mounted (!isMounted), so we call __render + this.__render(fiber); + } + else { + // we were mounted when render was called, but we aren't anymore, so we + // were actually about to be unmounted ; we can thus forget about this + // fiber + fiber.isCompleted = true; + __owl__.currentFiber = null; + } + }); + return scheduler.addFiber(fiber); + } + /** + * Destroy the component. This operation is quite complex: + * - it recursively destroy all children + * - call the willUnmount hooks if necessary + * - remove the dom node from the dom + * + * This should only be called manually if you created the component. Most + * components will be automatically destroyed. + */ + destroy() { + const __owl__ = this.__owl__; + if (!__owl__.isDestroyed) { + const el = this.el; + this.__destroy(__owl__.parent); + if (el) { + el.remove(); + } + } + } + /** + * This method is called by the component system whenever its props are + * updated. If it returns true, then the component will be rendered. + * Otherwise, it will skip the rendering (also, its props will not be updated) + */ + shouldUpdate(nextProps) { + return true; + } + /** + * Emit a custom event of type 'eventType' with the given 'payload' on the + * component's el, if it exists. However, note that the event will only bubble + * up to the parent DOM nodes. Thus, it must be called between mounted() and + * willUnmount(). + */ + trigger(eventType, payload) { + this.__trigger(this, eventType, payload); + } + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Private helper to perform a full destroy, from the point of view of an Owl + * component. It does not remove the el (this is done only once on the top + * level destroyed component, for performance reasons). + * + * The job of this method is mostly to call willUnmount hooks, and to perform + * all necessary internal cleanup. + * + * Note that it does not call the __callWillUnmount method to avoid visiting + * all children many times. + */ + __destroy(parent) { + const __owl__ = this.__owl__; + const isMounted = __owl__.isMounted; + if (isMounted) { + if (__owl__.willUnmountCB) { + __owl__.willUnmountCB(); + } + this.willUnmount(); + __owl__.isMounted = false; + } + const children = __owl__.children; + for (let key in children) { + children[key].__destroy(this); + } + if (parent) { + let id = __owl__.id; + delete parent.__owl__.children[id]; + __owl__.parent = null; + } + __owl__.isDestroyed = true; + delete __owl__.vnode; + if (__owl__.currentFiber) { + __owl__.currentFiber.isCompleted = true; + } + } + __callMounted() { + const __owl__ = this.__owl__; + __owl__.isMounted = true; + __owl__.currentFiber = null; + this.mounted(); + if (__owl__.mountedCB) { + __owl__.mountedCB(); + } + } + __callWillUnmount() { + const __owl__ = this.__owl__; + if (__owl__.willUnmountCB) { + __owl__.willUnmountCB(); + } + this.willUnmount(); + __owl__.isMounted = false; + if (this.__owl__.currentFiber) { + this.__owl__.currentFiber.isCompleted = true; + this.__owl__.currentFiber.root.counter = 0; + } + const children = __owl__.children; + for (let id in children) { + const comp = children[id]; + if (comp.__owl__.isMounted) { + comp.__callWillUnmount(); + } + } + } + /** + * Private trigger method, allows to choose the component which triggered + * the event in the first place + */ + __trigger(component, eventType, payload) { + if (this.el) { + const ev = new OwlEvent(component, eventType, { + bubbles: true, + cancelable: true, + detail: payload + }); + const triggerHook = this.env[portalSymbol]; + if (triggerHook) { + triggerHook(ev); + } + this.el.dispatchEvent(ev); + } + } + /** + * The __updateProps method is called by the t-component directive whenever + * it updates a component (so, when the parent template is rerendered). + */ + async __updateProps(nextProps, parentFiber, scope, vars) { + this.__owl__.scope = scope; + this.__owl__.vars = vars; + const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); + if (shouldUpdate) { + const __owl__ = this.__owl__; + const fiber = new Fiber(parentFiber, this, parentFiber.force, null); + if (!parentFiber.child) { + parentFiber.child = fiber; + } + else { + parentFiber.lastChild.sibling = fiber; + } + parentFiber.lastChild = fiber; + const defaultProps = this.constructor.defaultProps; + if (defaultProps) { + this.__applyDefaultProps(nextProps, defaultProps); + } + if (QWeb.dev) { + QWeb.utils.validateProps(this.constructor, nextProps); + } + await Promise.all([ + this.willUpdateProps(nextProps), + __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) + ]); + if (fiber.isCompleted) { + return; + } + this.props = nextProps; + this.__render(fiber); + } + } + /** + * Main patching method. We call the virtual dom patch method here to convert + * a virtual dom vnode into some actual dom. + */ + __patch(vnode) { + const __owl__ = this.__owl__; + const target = __owl__.vnode || document.createElement(vnode.sel); + __owl__.vnode = patch(target, vnode); + } + /** + * The __prepare method is only called by the t-component directive, when a + * subcomponent is created. It gets its scope and vars, if any, from the + * parent template. + */ + __prepare(parentFiber, scope, vars, cb) { + this.__owl__.scope = scope; + this.__owl__.vars = vars; + const fiber = new Fiber(parentFiber, this, parentFiber.force, null); + fiber.shouldPatch = false; + if (!parentFiber.child) { + parentFiber.child = fiber; + } + else { + parentFiber.lastChild.sibling = fiber; + } + parentFiber.lastChild = fiber; + this.__prepareAndRender(fiber, cb); + return fiber; + } + __getTemplate(qweb) { + let p = this.constructor; + if (!p.hasOwnProperty("_template")) { + // here, the component and none of its superclasses defines a static `template` + // key. So we fall back on looking for a template matching its name (or + // one of its subclass). + let template; + while ((template = p.name) && !(template in qweb.templates) && p !== Component) { + p = p.__proto__; + } + if (p === Component) { + throw new Error(`Could not find template for component "${this.constructor.name}"`); + } + else { + p._template = template; + } + } + return p._template; + } + async __prepareAndRender(fiber, cb) { + try { + await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]); + } + catch (e) { + fiber.handleError(e); + return Promise.resolve(); + } + if (this.__owl__.isDestroyed) { + return Promise.resolve(); + } + if (!fiber.isCompleted) { + this.__render(fiber); + cb(); + } + } + __render(fiber) { + const __owl__ = this.__owl__; + if (__owl__.observer) { + __owl__.observer.allowMutations = false; + } + let error; + try { + let vnode = __owl__.renderFn(this, { + handlers: __owl__.boundHandlers, + fiber: fiber + }); + // we iterate over the children to detect those that no longer belong to the + // current rendering: those ones, if not mounted yet, can (and have to) be + // destroyed right now, because they are not in the DOM, and thus we won't + // be notified later on (when patching), that they are removed from the DOM + for (let childKey in __owl__.children) { + let child = __owl__.children[childKey]; + if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) { + child.destroy(); + } + } + if (!vnode) { + throw new Error(`Rendering '${this.constructor.name}' did not return anything`); + } + fiber.vnode = vnode; + // we apply here the class information described on the component by the + // template (so, something like ) to the actual + // root vnode + if (__owl__.classObj) { + const data = vnode.data; + data.class = Object.assign(data.class || {}, __owl__.classObj); + } + } + catch (e) { + error = e; + } + if (__owl__.observer) { + __owl__.observer.allowMutations = true; + } + fiber.root.counter--; + fiber.isRendered = true; + if (error) { + fiber.handleError(error); + } + } + /** + * Only called by qweb t-component directive (when t-keepalive is set) + */ + __remount() { + const __owl__ = this.__owl__; + if (!__owl__.isMounted) { + __owl__.isMounted = true; + this.mounted(); + } + } + /** + * Apply default props (only top level). + * + * Note that this method does modify in place the props + */ + __applyDefaultProps(props, defaultProps) { + for (let propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + } + Component.template = null; + Component._template = null; + Component.current = null; + Component.components = {}; + Component.env = {}; + // expose scheduler s.t. it can be mocked for testing purposes + Component.scheduler = scheduler; + + /** + * The `Context` object provides a way to share data between an arbitrary number + * of component. Usually, data is passed from a parent to its children component, + * but when we have to deal with some mostly global information, this can be + * annoying, since each component will need to pass the information to each + * children, even though some or most of them will not use the information. + * + * With a `Context` object, each component can subscribe (with the `useContext` + * hook) to its state, and will be updated whenever the context state is updated. + */ + function partitionBy(arr, fn) { + let lastGroup = false; + let lastValue; + return arr.reduce((acc, cur) => { + let curVal = fn(cur); + if (lastGroup) { + if (curVal === lastValue) { + lastGroup.push(cur); + } + else { + lastGroup = false; + } + } + if (!lastGroup) { + lastGroup = [cur]; + acc.push(lastGroup); + } + lastValue = curVal; + return acc; + }, []); + } + class Context extends EventBus { + constructor(state = {}) { + super(); + this.rev = 1; + // mapping from component id to last observed context id + this.mapping = {}; + this.observer = new Observer(); + this.observer.notifyCB = () => { + // notify components in the next microtask tick to ensure that subscribers + // are notified only once for all changes that occur in the same micro tick + let rev = this.rev; + return Promise.resolve().then(() => { + if (rev === this.rev) { + this.__notifyComponents(); + } + }); + }; + this.state = this.observer.observe(state); + this.subscriptions.update = []; + } + /** + * Instead of using trigger to emit an update event, we actually implement + * our own function to do that. The reason is that we need to be smarter than + * a simple trigger function: we need to wait for parent components to be + * done before doing children components. More precisely, if an update + * as an effect of destroying a children, we do not want to call any code + * from the child, and certainly not render it. + * + * This method implements a simple grouping algorithm by depth. If we have + * connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify + * them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will + * be updated sequentially, but each components in a given group will be done in + * parallel. + * + * This is a very simple algorithm, but it avoids checking if a given + * component is a child of another. + */ + async __notifyComponents() { + const rev = ++this.rev; + const subscriptions = this.subscriptions.update; + const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1)); + for (let group of groups) { + const proms = group.map(sub => sub.callback.call(sub.owner, rev)); + // at this point, each component in the current group has registered a + // top level fiber in the scheduler. It could happen that rendering these + // components is done (if they have no children). This is why we manually + // flush the scheduler. This will force the scheduler to check + // immediately if they are done, which will cause their rendering + // promise to resolve earlier, which means that there is a chance of + // processing the next group in the same frame. + scheduler.flush(); + await Promise.all(proms); + } + } + } + /** + * The`useContext` hook is the normal way for a component to register themselve + * to context state changes. The `useContext` method returns the context state + */ + function useContext(ctx) { + const component = Component.current; + return useContextWithCB(ctx, component, component.render.bind(component)); + } + function useContextWithCB(ctx, component, method) { + const __owl__ = component.__owl__; + const id = __owl__.id; + const mapping = ctx.mapping; + if (id in mapping) { + return ctx.state; + } + if (!__owl__.observer) { + __owl__.observer = new Observer(); + __owl__.observer.notifyCB = component.render.bind(component); + } + const currentCB = __owl__.observer.notifyCB; + __owl__.observer.notifyCB = function () { + if (ctx.rev > mapping[id]) { + // in this case, the context has been updated since we were rendering + // last, and we do not need to render here with the observer. A + // rendering is coming anyway, with the correct props. + return; + } + currentCB(); + }; + mapping[id] = 0; + const renderFn = __owl__.renderFn; + __owl__.renderFn = function (comp, params) { + mapping[id] = ctx.rev; + return renderFn(comp, params); + }; + ctx.on("update", component, async (contextRev) => { + if (mapping[id] < contextRev) { + mapping[id] = contextRev; + await method(); + } + }); + const __destroy = component.__destroy; + component.__destroy = parent => { + ctx.off("update", component); + delete mapping[id]; + __destroy.call(component, parent); + }; + return ctx.state; + } + + /** + * Owl Hook System + * + * This file introduces the concept of hooks, similar to React or Vue hooks. + * We have currently an implementation of: + * - useState (reactive state) + * - onMounted + * - onWillUnmount + * - useRef + */ + // ----------------------------------------------------------------------------- + // useState + // ----------------------------------------------------------------------------- + /** + * This is the main way a component can be made reactive. The useState hook + * will return an observed object (or array). Changes to that value will then + * trigger a rerendering of the current component. + */ + function useState(state) { + const component = Component.current; + const __owl__ = component.__owl__; + if (!__owl__.observer) { + __owl__.observer = new Observer(); + __owl__.observer.notifyCB = component.render.bind(component); + } + return __owl__.observer.observe(state); + } + // ----------------------------------------------------------------------------- + // Life cycle hooks + // ----------------------------------------------------------------------------- + function makeLifecycleHook(method, reverse = false) { + if (reverse) { + return function (cb) { + const component = Component.current; + if (component.__owl__[method]) { + const current = component.__owl__[method]; + component.__owl__[method] = function () { + current.call(component); + cb.call(component); + }; + } + else { + component.__owl__[method] = cb; + } + }; + } + else { + return function (cb) { + const component = Component.current; + if (component.__owl__[method]) { + const current = component.__owl__[method]; + component.__owl__[method] = function () { + cb.call(component); + current.call(component); + }; + } + else { + component.__owl__[method] = cb; + } + }; + } + } + function makeAsyncHook(method) { + return function (cb) { + const component = Component.current; + if (component.__owl__[method]) { + const current = component.__owl__[method]; + component.__owl__[method] = function (...args) { + return Promise.all[(current.call(component, ...args), cb.call(component, ...args))]; }; } + else { + component.__owl__[method] = cb; + } + }; + } + const onMounted = makeLifecycleHook("mountedCB", true); + const onWillUnmount = makeLifecycleHook("willUnmountCB"); + const onWillPatch = makeLifecycleHook("willPatchCB"); + const onPatched = makeLifecycleHook("patchedCB", true); + const onWillStart = makeAsyncHook("willStartCB"); + const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB"); + function useRef(name) { + const __owl__ = Component.current.__owl__; + return { + get el() { + const val = __owl__.refs && __owl__.refs[name]; + if (val instanceof HTMLElement) { + return val; + } + else if (val instanceof Component) { + return val.el; + } + return null; + }, + get comp() { + const val = __owl__.refs && __owl__.refs[name]; + return val instanceof Component ? val : null; + } + }; + } + // ----------------------------------------------------------------------------- + // useSubEnv + // ----------------------------------------------------------------------------- + /** + * This hook is a simple way to let components use a sub environment. Note that + * like for all hooks, it is important that this is only called in the + * constructor method. + */ + function useSubEnv(nextEnv) { + const component = Component.current; + component.env = Object.assign(Object.create(component.env), nextEnv); + } + + var _hooks = /*#__PURE__*/Object.freeze({ + __proto__: null, + useState: useState, + onMounted: onMounted, + onWillUnmount: onWillUnmount, + onWillPatch: onWillPatch, + onPatched: onPatched, + onWillStart: onWillStart, + onWillUpdateProps: onWillUpdateProps, + useRef: useRef, + useSubEnv: useSubEnv + }); + + class Store extends Context { + constructor(config) { + super(config.state); + this.actions = config.actions; + this.env = config.env; + this.getters = {}; + this.updateFunctions = []; + if (config.getters) { + const firstArg = { + state: this.state, + getters: this.getters + }; + for (let g in config.getters) { + this.getters[g] = config.getters[g].bind(this, firstArg); + } + } } dispatch(action, ...payload) { if (!this.actions[action]) { throw new Error(`[Error] action ${action} is undefined`); } const result = this.actions[action]({ - commit: this.commit.bind(this), dispatch: this.dispatch.bind(this), env: this.env, state: this.state, getters: this.getters }, ...payload); - if (result instanceof Promise) { - return new Promise((resolve, reject) => { - result.then(() => resolve()); - result.catch(reject); - }); - } + return result; } - commit(type, ...payload) { - if (!this.mutations[type]) { - throw new Error(`[Error] mutation ${type} is undefined`); - } - this._commitLevel++; - this.observer.allowMutations = true; - const res = this.mutations[type].call(null, { - commit: this.commit.bind(this), - state: this.state, - getters: this.getters - }, ...payload); - if (this._commitLevel === 1) { - this.observer.allowMutations = false; - if (this.debug) { - this.history.push({ - state: this.state, - mutation: type, - payload: [...payload] - }); + } + const isStrictEqual = (a, b) => a === b; + function useStore(selector, options = {}) { + const component = Component.current; + const componentId = component.__owl__.id; + const store = options.store || component.env.store; + if (!(store instanceof Store)) { + throw new Error(`No store found when connecting '${component.constructor.name}'`); + } + let result = selector(store.state, component.props); + const hashFn = store.observer.revNumber.bind(store.observer); + let revNumber = hashFn(result); + const isEqual = options.isEqual || isStrictEqual; + if (!store.updateFunctions[componentId]) { + store.updateFunctions[componentId] = []; + } + function selectCompareUpdate(state, props) { + const oldResult = result; + result = selector(state, props); + const newRevNumber = hashFn(result); + if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) { + revNumber = newRevNumber; + if (options.onUpdate) { + options.onUpdate(result); } + return true; } - this._commitLevel--; - return res; + return false; + } + store.updateFunctions[componentId].push(function () { + return selectCompareUpdate(store.state, component.props); + }); + useContextWithCB(store, component, function () { + let shouldRender = false; + for (let fn of store.updateFunctions[componentId]) { + shouldRender = fn() || shouldRender; + } + if (shouldRender) { + return component.render(); + } + }); + onWillUpdateProps(props => { + selectCompareUpdate(store.state, props); + }); + const __destroy = component.__destroy; + component.__destroy = parent => { + delete store.updateFunctions[componentId]; + __destroy.call(component, parent); + }; + if (typeof result !== "object") { + return result; + } + return new Proxy(result, { + get(target, k) { + return result[k]; + }, + set(target, k, v) { + throw new Error("Store state should only be modified through actions"); + } + }); + } + function useDispatch(store) { + store = store || Component.current.env.store; + return store.dispatch.bind(store); + } + function useGetters(store) { + store = store || Component.current.env.store; + return store.getters; + } + + /** + * Owl Tags + * + * We have here a (very) small collection of tag functions: + * + * - xml + * + * The plan is to add a few other tags such as css, globalcss. + */ + /** + * XML tag helper for defining templates. With this, one can simply define + * an inline template with just the template xml: + * ```js + * class A extends Component { + * static template = xml`
some template
`; + * } + * ``` + */ + function xml(strings, ...args) { + const name = `__template__${QWeb.nextId++}`; + const value = String.raw(strings, ...args); + QWeb.registerTemplate(name, value); + return name; + } + + var _tags = /*#__PURE__*/Object.freeze({ + __proto__: null, + xml: xml + }); + + /** + * AsyncRoot + * + * Owl is by default asynchronous, and the user interface will wait for all its + * subcomponents to be rendered before updating the DOM. This is most of the + * time what we want, but in some cases, it makes sense to "detach" a component + * from this coordination. This is the goal of the AsyncRoot component. + */ + class AsyncRoot extends Component { + async __updateProps(nextProps, parentFiber) { + this.render(parentFiber.force); + } + } + AsyncRoot.template = xml ``; + + /** + * Portal + * + * The Portal component allows to render a part of a component outside it's DOM. + * It is for example useful for dialogs: for css reasons, dialogs are in general + * placed in a specific spot of the DOM (e.g. directly in the body). With the + * Portal, a component can conditionally specify in its tempate that it contains + * a dialog, and where this dialog should be inserted in the DOM. + * + * The Portal component ensures that the communication between the content of + * the Portal and its parent properly works: business events reaching the Portal + * are re-triggered on an empty node located in the parent's DOM. + */ + class Portal extends Component { + constructor(parent, props) { + super(parent, props); + // boolean to indicate whether or not we must listen to 'dom-appended' event + // to hook on the moment when the target is inserted into the DOM (because it + // is not when the portal is rendered) + this.doTargetLookUp = true; + // set of encountered events that need to be redirected + this._handledEvents = new Set(); + // function that will be the event's tunnel (needs to be an arrow function to + // avoid having to rebind `this`) + this._handlerTunnel = (ev) => { + ev.stopPropagation(); + this.__trigger(ev.originalComponent, ev.type, ev.detail); + }; + // Storing the parent's env + this.parentEnv = null; + // represents the element that is moved somewhere else + this.portal = null; + // the target where we will move `portal` + this.target = null; + this.parentEnv = parent ? parent.env : {}; + // put a callback in the env that is propagated to children s.t. portal can + // register an handler to those events just before children will trigger them + useSubEnv({ + [portalSymbol]: ev => { + if (!this._handledEvents.has(ev.type)) { + this.portal.elm.addEventListener(ev.type, this._handlerTunnel); + this._handledEvents.add(ev.type); + } + } + }); } /** - * Instead of using trigger to emit an update event, we actually implement - * our own function to do that. The reason is that we need to be smarter than - * a simple trigger function: we need to wait for parent components to be - * done before doing children components. The reason is that if an update - * as an effect of destroying a children, we do not want to call the - * mapStoreToProps function of the child, nor rendering it. - * - * This method is not optimal if we have a bunch of asynchronous components: - * we wait sequentially for each component to be completed before updating the - * next. However, the only things that matters is that children are updated - * after their parents. So, this could be optimized by being smarter, and - * updating all widgets concurrently, except for parents/children. + * At each DOM change, we must ensure that the portal contains exactly one + * child */ - async __notifyComponents() { - this._updateId++; - const current = this._updateId; - this._gettersCache = {}; - const subs = this.subscriptions.update || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true; - if (shouldCallback) { - await sub.callback.call(sub.owner, current); + __checkVNodeStructure(vnode) { + const children = vnode.children; + let countRealNodes = 0; + for (let child of children) { + if (child.sel) { + countRealNodes++; } } + if (countRealNodes !== 1) { + throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`); + } } - } - //------------------------------------------------------------------------------ - // Connect function - //------------------------------------------------------------------------------ - function revNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.rev; + /** + * Ensure the target is still there at whichever time we render + */ + __checkTargetPresence() { + if (!this.target || !document.contains(this.target)) { + throw new Error(`Could not find any match for "${this.props.target}"`); + } } - return 0; - } - function deepRevNumber(o) { - if (o !== null && typeof o === "object" && o.__owl__) { - return o.__owl__.deepRev; + /** + * Move the portal's element to the target + */ + __deployPortal() { + this.__checkTargetPresence(); + this.target.appendChild(this.portal.elm); } - return 0; - } - function connect(Comp, mapStoreToProps, options = {}) { - let hashFunction = options.hashFunction || null; - const getStore = options.getStore || (env => env.store); - if (!hashFunction) { - let deep = "deep" in options ? options.deep : true; - let defaultRevFunction = deep ? deepRevNumber : revNumber; - hashFunction = function ({ storeProps }, options) { - const { currentStoreProps } = options; - if ("__owl__" in storeProps) { - return defaultRevFunction(storeProps); + /** + * Override to remove from the DOM the element we have teleported + * + * @override + */ + __destroy(parent) { + if (this.portal && this.portal.elm) { + const displacedElm = this.portal.elm; + const parent = displacedElm.parentNode; + if (parent) { + parent.removeChild(displacedElm); } - let hash = 0; - for (let key in storeProps) { - const val = storeProps[key]; - const hashVal = defaultRevFunction(val); - if (hashVal === 0) { - if (val !== currentStoreProps[key]) { - options.didChange = true; - } - } - else { - hash += hashVal; - } - } - return hash; - }; + } + super.__destroy(parent); } - const Result = class extends Comp { - constructor(parent, props) { - const env = parent instanceof Component ? parent.env : parent; - const store = getStore(env); - const ownProps = Object.assign({}, props || {}); - const storeProps = mapStoreToProps(store.state, ownProps, store.getters); - const mergedProps = Object.assign({}, props || {}, storeProps); - super(parent, mergedProps); - this.__owl__.ownProps = ownProps; - this.__owl__.currentStoreProps = storeProps; - this.__owl__.store = store; - this.__owl__.storeHash = hashFunction({ - state: store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, { - currentStoreProps: storeProps - }); + /** + * Override to patch the element that has been teleported + * + * @override + */ + __patch(vnode) { + if (this.doTargetLookUp) { + const target = document.querySelector(this.props.target); + if (!target) { + this.env.qweb.on("dom-appended", this, () => { + this.doTargetLookUp = false; + this.env.qweb.off("dom-appended", this); + this.target = document.querySelector(this.props.target); + this.__deployPortal(); + }); + } + else { + this.doTargetLookUp = false; + this.target = target; + } } - /** - * We do not use the mounted hook here for a subtle reason: we want the - * updates to be called for the parents before the children. However, - * if we use the mounted hook, this will be done in the reverse order. - */ - __callMounted() { - this.__owl__.store.on("update", this, this.__checkUpdate); - super.__callMounted(); + this.__checkVNodeStructure(vnode); + const shouldDeploy = !this.portal && !this.doTargetLookUp; + if (!this.doTargetLookUp && !shouldDeploy) { + // Only on pure patching, provided the + // this.target's parent has not been unmounted + this.__checkTargetPresence(); } - willUnmount() { - this.__owl__.store.off("update", this); - super.willUnmount(); + const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel); + this.portal = patch(portalPatch, vnode.children[0]); + vnode.children = []; + super.__patch(vnode); + if (shouldDeploy) { + this.__deployPortal(); } - async __checkUpdate(updateId) { - if (updateId === this.__owl__.currentUpdateId) { + } + /** + * Override to set the env + */ + __trigger(component, eventType, payload) { + const env = this.env; + this.env = this.parentEnv; + super.__trigger(component, eventType, payload); + this.env = env; + } + } + Portal.template = xml ``; + Portal.props = { + target: { + type: String + } + }; + + class Link extends Component { + constructor() { + super(...arguments); + this.href = this.env.router.destToPath(this.props); + } + async willUpdateProps(nextProps) { + this.href = this.env.router.destToPath(nextProps); + } + get isActive() { + if (this.env.router.mode === "hash") { + return document.location.hash === this.href; + } + return document.location.pathname === this.href; + } + navigate(ev) { + // don't redirect with control keys + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + return; + } + // don't redirect on right click + if (ev.button !== undefined && ev.button !== 0) { + return; + } + // don't redirect if `target="_blank"` + if (ev.currentTarget && ev.currentTarget.getAttribute) { + const target = ev.currentTarget.getAttribute("target"); + if (/\b_blank\b/i.test(target)) { return; } - const ownProps = this.__owl__.ownProps; - const storeProps = mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters); - const options = { - currentStoreProps: this.__owl__.currentStoreProps - }; - const storeHash = hashFunction({ - state: this.__owl__.store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, options); - let didChange = options.didChange; - if (storeHash !== this.__owl__.storeHash) { - didChange = true; - this.__owl__.storeHash = storeHash; - } - if (didChange) { - this.__owl__.currentStoreProps = storeProps; - await this.__updateProps(ownProps, false); - } } - __updateProps(nextProps, forceUpdate, patchQueue) { - const __owl__ = this.__owl__; - __owl__.currentUpdateId = __owl__.store._updateId; - if (__owl__.ownProps !== nextProps) { - __owl__.currentStoreProps = mapStoreToProps(__owl__.store.state, nextProps, __owl__.store.getters); - } - __owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps); - return super.__updateProps(mergedProps, forceUpdate, patchQueue); - } - }; - // we assign here a unique name to the resulting anonymous class. - // this is necessary for Owl to be able to properly deduce templates. - // Otherwise, all connected components would have the same name, and then - // each component after the first will necessarily have the same template. - let name = `Connected${Comp.name}`; - Object.defineProperty(Result, "name", { value: name }); - return Result; + ev.preventDefault(); + this.env.router.navigate(this.props); + } } + Link.template = xml ` + + + + `; - /** - * Owl Utils - * - * We have here a small collection of utility functions: - * - * - whenReady - * - loadJS - * - loadTemplates - * - escape - * - debounce - */ - function whenReady(fn) { - return new Promise(function (resolve) { - if (document.readyState !== "loading") { - resolve(); + class RouteComponent extends Component { + get routeComponent() { + return this.env.router.currentRoute && this.env.router.currentRoute.component; + } + } + RouteComponent.template = xml ` + + + + `; + + const paramRegexp = /\{\{(.*?)\}\}/; + class Router { + constructor(env, routes, options = { mode: "history" }) { + this.currentRoute = null; + this.currentParams = null; + env.router = this; + this.mode = options.mode; + this.env = env; + this.routes = {}; + this.routeIds = []; + let nextId = 1; + for (let partialRoute of routes) { + if (!partialRoute.name) { + partialRoute.name = "__route__" + nextId++; + } + if (partialRoute.component) { + QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component); + } + if (partialRoute.redirect) { + this.validateDestination(partialRoute.redirect); + } + partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : []; + this.routes[partialRoute.name] = partialRoute; + this.routeIds.push(partialRoute.name); } - else { - document.addEventListener("DOMContentLoaded", resolve, false); + } + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + async start() { + this._listener = ev => this._navigate(this.currentPath(), ev); + window.addEventListener("popstate", this._listener); + if (this.mode === "hash") { + window.addEventListener("hashchange", this._listener); } - }).then(fn || function () { }); - } - const loadedScripts = {}; - function loadJS(url) { - if (url in loadedScripts) { - return loadedScripts[url]; - } - const promise = new Promise(function (resolve, reject) { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.src = url; - script.onload = function () { - resolve(); - }; - script.onerror = function () { - reject(`Error loading file '${url}'`); - }; - const head = document.head || document.getElementsByTagName("head")[0]; - head.appendChild(script); - }); - loadedScripts[url] = promise; - return promise; - } - async function loadTemplates(url) { - const result = await fetch(url); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); - } - let templates = await result.text(); - templates = templates.replace(//g, ""); - return templates; - } - function escape(str) { - if (str === undefined) { - return ""; - } - if (typeof str === "number") { - return String(str); - } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, "'") - .replace(/`/g, "`"); - } - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. - * - * Inspired by https://davidwalsh.name/javascript-debounce-function - */ - function debounce(func, wait, immediate) { - let timeout; - return function () { - const context = this; - const args = arguments; - function later() { - timeout = null; - if (!immediate) { - func.apply(context, args); + const result = await this.matchAndApplyRules(this.currentPath()); + if (result.type === "match") { + this.currentRoute = result.route; + this.currentParams = result.params; + const currentPath = this.routeToPath(result.route, result.params); + if (currentPath !== this.currentPath()) { + this.setUrlFromPath(currentPath); } } - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); + } + async navigate(to) { + const path = this.destToPath(to); + return this._navigate(path); + } + async _navigate(path, ev) { + const initialName = this.currentRouteName; + const initialParams = this.currentParams; + const result = await this.matchAndApplyRules(path); + if (result.type === "match") { + const finalPath = this.routeToPath(result.route, result.params); + const isPopStateEvent = ev && ev instanceof PopStateEvent; + if (!isPopStateEvent) { + this.setUrlFromPath(finalPath); + } + this.currentRoute = result.route; + this.currentParams = result.params; } - }; + else if (result.type === "nomatch") { + this.currentRoute = null; + this.currentParams = null; + } + const didChange = this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams); + if (didChange) { + this.env.qweb.forceUpdate(); + return true; + } + return false; + } + destToPath(dest) { + this.validateDestination(dest); + return dest.path || this.routeToPath(this.routes[dest.to], dest.params); + } + get currentRouteName() { + return this.currentRoute && this.currentRoute.name; + } + //-------------------------------------------------------------------------- + // Private helpers + //-------------------------------------------------------------------------- + setUrlFromPath(path) { + const separator = this.mode === "hash" ? location.pathname : ""; + const url = location.origin + separator + path; + if (url !== window.location.href) { + window.history.pushState({}, path, url); + } + } + validateDestination(dest) { + if ((!dest.path && !dest.to) || (dest.path && dest.to)) { + throw new Error(`Invalid destination: ${JSON.stringify(dest)}`); + } + } + routeToPath(route, params) { + const path = route.path; + const parts = path.split("/"); + const l = parts.length; + for (let i = 0; i < l; i++) { + const part = parts[i]; + const match = part.match(paramRegexp); + if (match) { + const key = match[1].split(".")[0]; + parts[i] = params[key]; + } + } + const prefix = this.mode === "hash" ? "#" : ""; + return prefix + parts.join("/"); + } + currentPath() { + let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1); + return result || "/"; + } + match(path) { + for (let routeId of this.routeIds) { + let route = this.routes[routeId]; + let params = this.getRouteParams(route, path); + if (params) { + return { + type: "match", + route: route, + params: params + }; + } + } + return { type: "nomatch" }; + } + async matchAndApplyRules(path) { + const result = this.match(path); + if (result.type === "match") { + return this.applyRules(result); + } + return result; + } + async applyRules(matchResult) { + const route = matchResult.route; + if (route.redirect) { + const path = this.destToPath(route.redirect); + return this.matchAndApplyRules(path); + } + if (route.beforeRouteEnter) { + const result = await route.beforeRouteEnter({ + env: this.env, + from: this.currentRoute, + to: route + }); + if (result === false) { + return { type: "cancelled" }; + } + else if (result !== true) { + // we want to navigate to another destination + const path = this.destToPath(result); + return this.matchAndApplyRules(path); + } + } + return matchResult; + } + getRouteParams(route, path) { + if (route.path === "*") { + return {}; + } + if (path.startsWith("#")) { + path = path.slice(1); + } + const descrParts = route.path.split("/"); + const targetParts = path.split("/"); + const l = descrParts.length; + if (l !== targetParts.length) { + return false; + } + const result = {}; + for (let i = 0; i < l; i++) { + const descr = descrParts[i]; + let target = targetParts[i]; + const match = descr.match(paramRegexp); + if (match) { + const [key, suffix] = match[1].split("."); + if (suffix === "number") { + target = parseInt(target, 10); + } + result[key] = target; + } + else if (descr !== target) { + return false; + } + } + return result; + } + } + function findParams(str) { + const globalParamRegexp = /\{\{(.*?)\}\}/g; + const result = []; + let m; + do { + m = globalParamRegexp.exec(str); + if (m) { + result.push(m[1].split(".")[0]); + } + } while (m); + return result; } - - var _utils = /*#__PURE__*/Object.freeze({ - whenReady: whenReady, - loadJS: loadJS, - loadTemplates: loadTemplates, - escape: escape, - debounce: debounce - }); /** * This file is the main file packaged by rollup (see rollup.config.js). From @@ -3583,36 +4828,39 @@ * * Note that dynamic values, such as a date or a commit hash are added by rollup */ - const __info__ = {}; - Object.defineProperty(__info__, "mode", { - get() { - return QWeb.dev ? "dev" : "prod"; - }, - set(mode) { - QWeb.dev = mode === "dev"; - if (QWeb.dev) { - const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`; - console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`); - } - else { - console.log(`Owl is now running in 'prod' mode.`); - } - } - }); + const Context$1 = Context; + const useState$1 = useState; + const core = { EventBus, Observer }; + const router = { Router, RouteComponent, Link }; + const Store$1 = Store; const utils = _utils; + const tags = _tags; + const misc = { AsyncRoot, Portal }; + const hooks$1 = Object.assign({}, _hooks, { + useContext: useContext, + useDispatch: useDispatch, + useGetters: useGetters, + useStore: useStore + }); + const __info__ = {}; exports.Component = Component; - exports.EventBus = EventBus; - exports.Observer = Observer; + exports.Context = Context$1; exports.QWeb = QWeb; - exports.Store = Store; + exports.Store = Store$1; exports.__info__ = __info__; - exports.connect = connect; + exports.config = config; + exports.core = core; + exports.hooks = hooks$1; + exports.misc = misc; + exports.router = router; + exports.tags = tags; + exports.useState = useState$1; exports.utils = utils; - exports.__info__.version = '0.16.0'; - exports.__info__.date = '2019-06-28T09:20:47.478Z'; - exports.__info__.hash = 'd381e85'; + exports.__info__.version = '1.0.0-beta1'; + exports.__info__.date = '2019-12-03T07:54:22.133Z'; + exports.__info__.hash = '42aa9d3'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/tools/benchmarks/owl-0.16.0/templates.xml b/tools/benchmarks/owl-1.0.0-beta1/templates.xml similarity index 88% rename from tools/benchmarks/owl-0.16.0/templates.xml rename to tools/benchmarks/owl-1.0.0-beta1/templates.xml index 0a680ea0..c8724c0a 100644 --- a/tools/benchmarks/owl-0.16.0/templates.xml +++ b/tools/benchmarks/owl-1.0.0-beta1/templates.xml @@ -12,11 +12,11 @@
- +
- +
diff --git a/tools/index.html b/tools/index.html index b6c53e7e..d8a378f7 100644 --- a/tools/index.html +++ b/tools/index.html @@ -28,19 +28,15 @@