+
+ OWL 0.13.0 Benchmark
+
+
+
+
+
+
+
+
diff --git a/extras/benchmarks/owl-0.13.0/owl.js b/extras/benchmarks/owl-0.13.0/owl.js
new file mode 100644
index 00000000..17ff8c7c
--- /dev/null
+++ b/extras/benchmarks/owl-0.13.0/owl.js
@@ -0,0 +1,3186 @@
+(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;
+ }
+ 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();
+ }
+ _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) {
+ 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: 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(" ");
+ },
+ 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++;
+ 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 {
+ // 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 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))})`;
+ }
+ 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 {
+ //--------------------------------------------------------------------------
+ // 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);
+ }
+ }
+ /**
+ * Sets a key (from the state) to a specific value. This is mostly useful to
+ * work around the limitation in observed value with new keys.
+ */
+ set(target, key, value) {
+ this.__owl__.observer.set(target, key, value);
+ }
+ /**
+ * 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 (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-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
+ //------------------------------------------------------------------------------
+ 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.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
+ * // 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 }) {
+ 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.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 || 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;`);
+ 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}](); }`);
+ }
+ });
+
+ 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.13.0';
+ exports.__info__.date = '2019-06-05T08:48:29.138Z';
+ exports.__info__.hash = '7d3c374';
+ exports.__info__.url = 'https://github.com/odoo/owl';
+
+}(this.owl = this.owl || {}));
diff --git a/extras/benchmarks/owl-0.13.0/templates.xml b/extras/benchmarks/owl-0.13.0/templates.xml
new file mode 100644
index 00000000..a1bff7de
--- /dev/null
+++ b/extras/benchmarks/owl-0.13.0/templates.xml
@@ -0,0 +1,34 @@
+
+
+
+
Number of msg:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/index.html b/extras/index.html
index 3ee6507e..ca5b800f 100644
--- a/extras/index.html
+++ b/extras/index.html
@@ -33,6 +33,7 @@