-
- OWL 0.10.0 Benchmark
-
-
-
-
-
-
-
-
diff --git a/tools/benchmarks/owl-0.10.0/owl.js b/tools/benchmarks/owl-0.10.0/owl.js
deleted file mode 100644
index c59fe751..00000000
--- a/tools/benchmarks/owl-0.10.0/owl.js
+++ /dev/null
@@ -1,2685 +0,0 @@
-(function (exports) {
- 'use strict';
-
- /**
- * We define here a simple event bus: it can
- * - emit events
- * - add/remove listeners.
- *
- * This is a useful pattern of communication in many cases. For OWL, each
- * components and stores are event buses.
- */
- //------------------------------------------------------------------------------
- // EventBus
- //------------------------------------------------------------------------------
- class EventBus {
- constructor() {
- this.subscriptions = {};
- }
- /**
- * Add a listener for the 'eventType' events.
- *
- * Note that the 'owner' of this event can be anything, but will more likely
- * be a widget or a class. The idea is that the callback will be called with
- * the proper owner bound.
- *
- * Also, the owner should be kind of unique. This will be used to remove the
- * listener.
- */
- on(eventType, owner, callback) {
- if (!callback) {
- throw new Error("Missing callback");
- }
- if (!this.subscriptions[eventType]) {
- this.subscriptions[eventType] = [];
- }
- this.subscriptions[eventType].push({
- owner,
- callback
- });
- }
- /**
- * Remove a listener
- */
- off(eventType, owner) {
- const subs = this.subscriptions[eventType];
- if (subs) {
- this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
- }
- }
- /**
- * Emit an event of type 'eventType'. Any extra arguments will be passed to
- * the listeners callback.
- */
- trigger(eventType, ...args) {
- const subs = this.subscriptions[eventType] || [];
- for (let sub of subs) {
- sub.callback.call(sub.owner, ...args);
- }
- }
- /**
- * Remove all subscriptions.
- */
- clear() {
- this.subscriptions = {};
- }
- }
-
- /**
- * Owl Observer
- *
- * This code contains the logic that allows Owl to observe and react to state
- * changes.
- *
- * This is a Observer class that can observe any JS values. The way it works
- * can be summarized thusly:
- * - primitive values are not observed at all
- * - Objects are observed by replacing all their keys with getters/setters
- * (recursively)
- * - Arrays are observed by replacing their prototype with a customized version,
- * which wrap some methods to allow the tracking of each state change.
- *
- * Note that this code is inspired by Vue.
- */
- //------------------------------------------------------------------------------
- // Modified Array prototype
- //------------------------------------------------------------------------------
- // we define here a new modified Array prototype, which basically override all
- // Array methods that change some state to be able to track their changes
- const methodsToPatch = [
- "push",
- "pop",
- "shift",
- "unshift",
- "splice",
- "sort",
- "reverse"
- ];
- const ArrayProto = Array.prototype;
- const ModifiedArrayProto = Object.create(ArrayProto);
- for (let method of methodsToPatch) {
- const initialMethod = ArrayProto[method];
- ModifiedArrayProto[method] = function (...args) {
- if (!this.__observer__.allowMutations) {
- throw new Error(`Array cannot be changed here")`);
- }
- this.__observer__.notifyChange();
- this.__owl__.rev++;
- let parent = this;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent));
- let inserted;
- switch (method) {
- case "push":
- case "unshift":
- inserted = args;
- break;
- case "splice":
- inserted = args.slice(2);
- break;
- }
- if (inserted) {
- for (let elem of inserted) {
- this.__observer__.observe(elem, this);
- }
- }
- return initialMethod.call(this, ...args);
- };
- }
- //------------------------------------------------------------------------------
- // Observer
- //------------------------------------------------------------------------------
- class Observer {
- constructor() {
- this.rev = 1;
- this.allowMutations = true;
- this.dirty = false;
- }
- notifyCB() { }
- notifyChange() {
- this.rev++;
- this.dirty = true;
- Promise.resolve().then(() => {
- if (this.dirty) {
- this.dirty = false;
- this.notifyCB();
- }
- });
- }
- observe(value, parent) {
- if (value === null) {
- // fun fact: typeof null === 'object'
- return;
- }
- if (typeof value !== "object") {
- return;
- }
- if ("__owl__" in value) {
- // already observed
- value.__owl__.parent = parent;
- return;
- }
- if (Array.isArray(value)) {
- this._observeArr(value, parent);
- }
- else {
- this._observeObj(value, parent);
- }
- }
- set(target, key, value) {
- this._addProp(target, key, value);
- target.__owl__.rev++;
- this.notifyChange();
- }
- _observeObj(obj, parent) {
- const keys = Object.keys(obj);
- obj.__owl__ = { rev: 1, deepRev: 1, parent };
- Object.defineProperty(obj, "__owl__", { enumerable: false });
- for (let key of keys) {
- this._addProp(obj, key, obj[key]);
- }
- }
- _observeArr(arr, parent) {
- arr.__owl__ = { rev: 1, deepRev: 1, parent };
- Object.defineProperty(arr, "__owl__", { enumerable: false });
- arr.__proto__ = Object.create(ModifiedArrayProto);
- arr.__proto__.__observer__ = this;
- for (let i = 0; i < arr.length; i++) {
- this.observe(arr[i], arr);
- }
- }
- _addProp(obj, key, value) {
- var self = this;
- Object.defineProperty(obj, key, {
- enumerable: true,
- get() {
- return value;
- },
- set(newVal) {
- if (newVal !== value) {
- if (!self.allowMutations) {
- throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`);
- }
- value = newVal;
- self.observe(newVal, obj);
- obj.__owl__.rev++;
- let parent = obj;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent) && parent !== obj);
- self.notifyChange();
- }
- }
- });
- this.observe(value, obj);
- }
- }
-
- /**
- * Owl VDOM
- *
- * This file contains an implementation of a virtual DOM, which is a system that
- * can generate in-memory representations of a DOM tree, compare them, and
- * eventually change a concrete DOM tree to match its representation, in an
- * hopefully efficient way.
- *
- * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our
- * needs (see https://github.com/snabbdom/snabbdom).
- *
- * The main exported values are:
- * - interface VNode
- * - h function (a helper function to generate a vnode)
- * - patch function (to apply a vnode to an actual DOM node)
- */
- function vnode(sel, data, children, text, elm) {
- let key = data === undefined ? undefined : data.key;
- return {
- sel: sel,
- data: data,
- children: children,
- text: text,
- elm: elm,
- key: key
- };
- }
- //------------------------------------------------------------------------------
- // snabbdom.ts
- //------------------------------------------------------------------------------
- function isUndef(s) {
- return s === undefined;
- }
- function isDef(s) {
- return s !== undefined;
- }
- const emptyNode = vnode("", {}, [], undefined, undefined);
- function sameVnode(vnode1, vnode2) {
- return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
- }
- function isVnode(vnode) {
- return vnode.sel !== undefined;
- }
- function createKeyToOldIdx(children, beginIdx, endIdx) {
- let i, map = {}, key, ch;
- for (i = beginIdx; i <= endIdx; ++i) {
- ch = children[i];
- if (ch != null) {
- key = ch.key;
- if (key !== undefined)
- map[key] = i;
- }
- }
- return map;
- }
- const hooks = [
- "create",
- "update",
- "remove",
- "destroy",
- "pre",
- "post"
- ];
- function init(modules, domApi) {
- let i, j, cbs = {};
- const api = domApi !== undefined ? domApi : htmlDomApi;
- for (i = 0; i < hooks.length; ++i) {
- cbs[hooks[i]] = [];
- for (j = 0; j < modules.length; ++j) {
- const hook = modules[j][hooks[i]];
- if (hook !== undefined) {
- cbs[hooks[i]].push(hook);
- }
- }
- }
- function emptyNodeAt(elm) {
- const id = elm.id ? "#" + elm.id : "";
- const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
- return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
- }
- function createRmCb(childElm, listeners) {
- return function rmCb() {
- if (--listeners === 0) {
- const parent = api.parentNode(childElm);
- api.removeChild(parent, childElm);
- }
- };
- }
- function createElm(vnode, insertedVnodeQueue) {
- let i, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.init))) {
- i(vnode);
- data = vnode.data;
- }
- }
- let children = vnode.children, sel = vnode.sel;
- if (sel === "!") {
- if (isUndef(vnode.text)) {
- vnode.text = "";
- }
- vnode.elm = api.createComment(vnode.text);
- }
- else if (sel !== undefined) {
- // Parse selector
- const hashIdx = sel.indexOf("#");
- const dotIdx = sel.indexOf(".", hashIdx);
- const hash = hashIdx > 0 ? hashIdx : sel.length;
- const dot = dotIdx > 0 ? dotIdx : sel.length;
- const tag = hashIdx !== -1 || dotIdx !== -1
- ? sel.slice(0, Math.min(hash, dot))
- : sel;
- const elm = (vnode.elm =
- isDef(data) && isDef((i = data.ns))
- ? api.createElementNS(i, tag)
- : api.createElement(tag));
- if (hash < dot)
- elm.setAttribute("id", sel.slice(hash + 1, dot));
- if (dotIdx > 0)
- elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
- for (i = 0; i < cbs.create.length; ++i)
- cbs.create[i](emptyNode, vnode);
- if (array(children)) {
- for (i = 0; i < children.length; ++i) {
- const ch = children[i];
- if (ch != null) {
- api.appendChild(elm, createElm(ch, insertedVnodeQueue));
- }
- }
- }
- else if (primitive(vnode.text)) {
- api.appendChild(elm, api.createTextNode(vnode.text));
- }
- i = vnode.data.hook; // Reuse variable
- if (isDef(i)) {
- if (i.create)
- i.create(emptyNode, vnode);
- if (i.insert)
- insertedVnodeQueue.push(vnode);
- }
- }
- else {
- vnode.elm = api.createTextNode(vnode.text);
- }
- return vnode.elm;
- }
- function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
- for (; startIdx <= endIdx; ++startIdx) {
- const ch = vnodes[startIdx];
- if (ch != null) {
- api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
- }
- }
- }
- function invokeDestroyHook(vnode) {
- let i, j, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.destroy)))
- i(vnode);
- for (i = 0; i < cbs.destroy.length; ++i)
- cbs.destroy[i](vnode);
- if (vnode.children !== undefined) {
- for (j = 0; j < vnode.children.length; ++j) {
- i = vnode.children[j];
- if (i != null && typeof i !== "string") {
- invokeDestroyHook(i);
- }
- }
- }
- }
- }
- function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
- for (; startIdx <= endIdx; ++startIdx) {
- let i, listeners, rm, ch = vnodes[startIdx];
- if (ch != null) {
- if (isDef(ch.sel)) {
- invokeDestroyHook(ch);
- listeners = cbs.remove.length + 1;
- rm = createRmCb(ch.elm, listeners);
- for (i = 0; i < cbs.remove.length; ++i)
- cbs.remove[i](ch, rm);
- if (isDef((i = ch.data)) &&
- isDef((i = i.hook)) &&
- isDef((i = i.remove))) {
- i(ch, rm);
- }
- else {
- rm();
- }
- }
- else {
- // Text node
- api.removeChild(parentElm, ch.elm);
- }
- }
- }
- }
- function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
- let oldStartIdx = 0, newStartIdx = 0;
- let oldEndIdx = oldCh.length - 1;
- let oldStartVnode = oldCh[0];
- let oldEndVnode = oldCh[oldEndIdx];
- let newEndIdx = newCh.length - 1;
- let newStartVnode = newCh[0];
- let newEndVnode = newCh[newEndIdx];
- let oldKeyToIdx;
- let idxInOld;
- let elmToMove;
- let before;
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
- if (oldStartVnode == null) {
- oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
- }
- else if (oldEndVnode == null) {
- oldEndVnode = oldCh[--oldEndIdx];
- }
- else if (newStartVnode == null) {
- newStartVnode = newCh[++newStartIdx];
- }
- else if (newEndVnode == null) {
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newStartVnode)) {
- patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
- oldStartVnode = oldCh[++oldStartIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else if (sameVnode(oldEndVnode, newEndVnode)) {
- patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
- oldEndVnode = oldCh[--oldEndIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newEndVnode)) {
- // Vnode moved right
- patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
- oldStartVnode = oldCh[++oldStartIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldEndVnode, newStartVnode)) {
- // Vnode moved left
- patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode = oldCh[--oldEndIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- if (oldKeyToIdx === undefined) {
- oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
- }
- idxInOld = oldKeyToIdx[newStartVnode.key];
- if (isUndef(idxInOld)) {
- // New element
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- elmToMove = oldCh[idxInOld];
- if (elmToMove.sel !== newStartVnode.sel) {
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- }
- else {
- patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
- oldCh[idxInOld] = undefined;
- api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
- }
- newStartVnode = newCh[++newStartIdx];
- }
- }
- }
- if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
- if (oldStartIdx > oldEndIdx) {
- before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
- addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
- }
- else {
- removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
- }
- }
- }
- function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
- let i, hook;
- if (isDef((i = vnode.data)) &&
- isDef((hook = i.hook)) &&
- isDef((i = hook.prepatch))) {
- i(oldVnode, vnode);
- }
- const elm = (vnode.elm = oldVnode.elm);
- let oldCh = oldVnode.children;
- let ch = vnode.children;
- if (oldVnode === vnode)
- return;
- if (vnode.data !== undefined) {
- for (i = 0; i < cbs.update.length; ++i)
- cbs.update[i](oldVnode, vnode);
- i = vnode.data.hook;
- if (isDef(i) && isDef((i = i.update)))
- i(oldVnode, vnode);
- }
- if (isUndef(vnode.text)) {
- if (isDef(oldCh) && isDef(ch)) {
- if (oldCh !== ch)
- updateChildren(elm, oldCh, ch, insertedVnodeQueue);
- }
- else if (isDef(ch)) {
- if (isDef(oldVnode.text))
- api.setTextContent(elm, "");
- addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
- }
- else if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- else if (isDef(oldVnode.text)) {
- api.setTextContent(elm, "");
- }
- }
- else if (oldVnode.text !== vnode.text) {
- if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- api.setTextContent(elm, vnode.text);
- }
- if (isDef(hook) && isDef((i = hook.postpatch))) {
- i(oldVnode, vnode);
- }
- }
- return function patch(oldVnode, vnode) {
- let i, elm, parent;
- const insertedVnodeQueue = [];
- for (i = 0; i < cbs.pre.length; ++i)
- cbs.pre[i]();
- if (!isVnode(oldVnode)) {
- oldVnode = emptyNodeAt(oldVnode);
- }
- if (sameVnode(oldVnode, vnode)) {
- patchVnode(oldVnode, vnode, insertedVnodeQueue);
- }
- else {
- elm = oldVnode.elm;
- parent = api.parentNode(elm);
- createElm(vnode, insertedVnodeQueue);
- if (parent !== null) {
- api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
- removeVnodes(parent, [oldVnode], 0, 0);
- }
- }
- for (i = 0; i < insertedVnodeQueue.length; ++i) {
- insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
- }
- for (i = 0; i < cbs.post.length; ++i)
- cbs.post[i]();
- return vnode;
- };
- }
- //------------------------------------------------------------------------------
- // is.ts
- //------------------------------------------------------------------------------
- const array = Array.isArray;
- function primitive(s) {
- return typeof s === "string" || typeof s === "number";
- }
- function createElement(tagName) {
- return document.createElement(tagName);
- }
- function createElementNS(namespaceURI, qualifiedName) {
- return document.createElementNS(namespaceURI, qualifiedName);
- }
- function createTextNode(text) {
- return document.createTextNode(text);
- }
- function createComment(text) {
- return document.createComment(text);
- }
- function insertBefore(parentNode, newNode, referenceNode) {
- parentNode.insertBefore(newNode, referenceNode);
- }
- function removeChild(node, child) {
- node.removeChild(child);
- }
- function appendChild(node, child) {
- node.appendChild(child);
- }
- function parentNode(node) {
- return node.parentNode;
- }
- function nextSibling(node) {
- return node.nextSibling;
- }
- function tagName(elm) {
- return elm.tagName;
- }
- function setTextContent(node, text) {
- node.textContent = text;
- }
- function getTextContent(node) {
- return node.textContent;
- }
- function isElement(node) {
- return node.nodeType === 1;
- }
- function isText(node) {
- return node.nodeType === 3;
- }
- function isComment(node) {
- return node.nodeType === 8;
- }
- const htmlDomApi = {
- createElement,
- createElementNS,
- createTextNode,
- createComment,
- insertBefore,
- removeChild,
- appendChild,
- parentNode,
- nextSibling,
- tagName,
- setTextContent,
- getTextContent,
- isElement,
- isText,
- isComment
- };
- function addNS(data, children, sel) {
- data.ns = "http://www.w3.org/2000/svg";
- if (sel !== "foreignObject" && children !== undefined) {
- for (let i = 0; i < children.length; ++i) {
- let childData = children[i].data;
- if (childData !== undefined) {
- addNS(childData, children[i].children, children[i].sel);
- }
- }
- }
- }
- function h(sel, b, c) {
- var data = {}, children, text, i;
- if (c !== undefined) {
- data = b;
- if (array(c)) {
- children = c;
- }
- else if (primitive(c)) {
- text = c;
- }
- else if (c && c.sel) {
- children = [c];
- }
- }
- else if (b !== undefined) {
- if (array(b)) {
- children = b;
- }
- else if (primitive(b)) {
- text = b;
- }
- else if (b && b.sel) {
- children = [b];
- }
- else {
- data = b;
- }
- }
- if (children !== undefined) {
- for (i = 0; i < children.length; ++i) {
- if (primitive(children[i]))
- children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
- }
- }
- if (sel[0] === "s" &&
- sel[1] === "v" &&
- sel[2] === "g" &&
- (sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
- addNS(data, children, sel);
- }
- return vnode(sel, data, children, text, undefined);
- }
- function updateProps(oldVnode, vnode) {
- var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
- if (!oldProps && !props)
- return;
- if (oldProps === props)
- return;
- oldProps = oldProps || {};
- props = props || {};
- for (key in oldProps) {
- if (!props[key]) {
- delete elm[key];
- }
- }
- for (key in props) {
- cur = props[key];
- old = oldProps[key];
- if (old !== cur && (key !== "value" || elm[key] !== cur)) {
- elm[key] = cur;
- }
- }
- }
- const propsModule = {
- create: updateProps,
- update: updateProps
- };
- function invokeHandler(handler, vnode, event) {
- if (typeof handler === "function") {
- // call function handler
- handler.call(vnode, event, vnode);
- }
- else if (typeof handler === "object") {
- // call handler with arguments
- if (typeof handler[0] === "function") {
- // special case for single argument for performance
- if (handler.length === 2) {
- handler[0].call(vnode, handler[1], event, vnode);
- }
- else {
- var args = handler.slice(1);
- args.push(event);
- args.push(vnode);
- handler[0].apply(vnode, args);
- }
- }
- else {
- // call multiple handlers
- for (var i = 0; i < handler.length; i++) {
- invokeHandler(handler[i], vnode, event);
- }
- }
- }
- }
- function handleEvent(event, vnode) {
- var name = event.type, on = vnode.data.on;
- // call event handler(s) if exists
- if (on && on[name]) {
- invokeHandler(on[name], vnode, event);
- }
- }
- function createListener() {
- return function handler(event) {
- handleEvent(event, handler.vnode);
- };
- }
- function updateEventListeners(oldVnode, vnode) {
- var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name;
- // optimization for reused immutable handlers
- if (oldOn === on) {
- return;
- }
- // remove existing listeners which no longer used
- if (oldOn && oldListener) {
- // if element changed or deleted we remove all existing listeners unconditionally
- if (!on) {
- for (name in oldOn) {
- // remove listener if element was changed or existing listeners removed
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- else {
- for (name in oldOn) {
- // remove listener if existing listener removed
- if (!on[name]) {
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- }
- }
- // add new listeners which has not already attached
- if (on) {
- // reuse existing listener or create new
- var listener = (vnode.listener =
- oldVnode.listener || createListener());
- // update vnode for listener
- listener.vnode = vnode;
- // if element changed or added we add all needed listeners unconditionally
- if (!oldOn) {
- for (name in on) {
- // add listener if element was changed or new listeners added
- elm.addEventListener(name, listener, false);
- }
- }
- else {
- for (name in on) {
- // add listener if new listener added
- if (!oldOn[name]) {
- elm.addEventListener(name, listener, false);
- }
- }
- }
- }
- }
- const eventListenersModule = {
- create: updateEventListeners,
- update: updateEventListeners,
- destroy: updateEventListeners
- };
- const xlinkNS = "http://www.w3.org/1999/xlink";
- const xmlNS = "http://www.w3.org/XML/1998/namespace";
- const colonChar = 58;
- const xChar = 120;
- function updateAttrs(oldVnode, vnode) {
- var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs;
- if (!oldAttrs && !attrs)
- return;
- if (oldAttrs === attrs)
- return;
- oldAttrs = oldAttrs || {};
- attrs = attrs || {};
- // update modified attributes, add new attributes
- for (key in attrs) {
- const cur = attrs[key];
- const old = oldAttrs[key];
- if (old !== cur) {
- if (cur === true) {
- elm.setAttribute(key, "");
- }
- else if (cur === false) {
- elm.removeAttribute(key);
- }
- else {
- if (key.charCodeAt(0) !== xChar) {
- elm.setAttribute(key, cur);
- }
- else if (key.charCodeAt(3) === colonChar) {
- // Assume xml namespace
- elm.setAttributeNS(xmlNS, key, cur);
- }
- else if (key.charCodeAt(5) === colonChar) {
- // Assume xlink namespace
- elm.setAttributeNS(xlinkNS, key, cur);
- }
- else {
- elm.setAttribute(key, cur);
- }
- }
- }
- }
- // remove removed attributes
- // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
- // the other option is to remove all attributes with value == undefined
- for (key in oldAttrs) {
- if (!(key in attrs)) {
- elm.removeAttribute(key);
- }
- }
- }
- const attrsModule = {
- create: updateAttrs,
- update: updateAttrs
- };
- const patch = init([eventListenersModule, attrsModule, propsModule]);
-
- //------------------------------------------------------------------------------
- // Widget
- //------------------------------------------------------------------------------
- let nextId = 1;
- class Component extends EventBus {
- //--------------------------------------------------------------------------
- // Lifecycle
- //--------------------------------------------------------------------------
- /**
- * Creates an instance of Component.
- *
- * The root widget of a component tree needs an environment:
- *
- * ```javascript
- * const root = new RootWidget(env, props);
- * ```
- *
- * Every other widget simply needs a reference to its parent:
- *
- * ```javascript
- * const child = new SomeWidget(parent, props);
- * ```
- *
- * Note that most of the time, only the root widget needs to be created by
- * hand. Other widgets should be created automatically by the framework (with
- * the t-widget directive in a template)
- */
- constructor(parent, props) {
- super();
- this.template = "default";
- this.inlineTemplate = null;
- this.refs = {};
- // is this a good idea?
- // Pro: if props is empty, we can create easily a widget
- // Con: this is not really safe
- // Pro: but creating widget (by a template) is always unsafe anyway
- this.props = props || {};
- let id = nextId++;
- let p = null;
- if (parent instanceof Component) {
- p = parent;
- this.env = parent.env;
- parent.__owl__.children[id] = this;
- }
- else {
- this.env = parent;
- }
- this.__owl__ = {
- id: id,
- vnode: null,
- isStarted: false,
- isMounted: false,
- isDestroyed: false,
- parent: p,
- children: {},
- cmap: {},
- renderId: 1,
- renderPromise: null,
- renderProps: props || null,
- boundHandlers: {}
- };
- }
- get el() {
- return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
- }
- /**
- * willStart is an asynchronous hook that can be implemented to perform some
- * action before the initial rendering of a component.
- *
- * It will be called exactly once before the initial rendering. It is useful
- * in some cases, for example, to load external assets (such as a JS library)
- * before the widget is rendered.
- *
- * Note that a slow willStart method will slow down the rendering of the user
- * interface. Therefore, some effort should be made to make this method as
- * fast as possible.
- *
- * Note: this method should not be called manually.
- */
- async willStart() { }
- /**
- * mounted is a hook that is called each time a component is attached to the
- * DOM. This is a good place to add some listeners, or to interact with the
- * DOM, if the component needs to perform some measure for example.
- *
- * Note: this method should not be called manually.
- *
- * @see willUnmount
- */
- mounted() { }
- /**
- * The willUpdateProps is an asynchronous hook, called just before new props
- * are set. This is useful if the component needs some asynchronous task
- * performed, depending on the props (for example, assuming that the props are
- * some record Id, fetching the record data).
- *
- * This hook is not called during the first render (but willStart is called
- * and performs a similar job).
- */
- async willUpdateProps(nextProps) { }
- /**
- * The willPatch hook is called just before the DOM patching process starts.
- * It is not called on the initial render. This is useful to get some
- * information which are in the DOM. For example, the current position of the
- * scrollbar
- *
- * The return value of willPatch will be given to the patched function.
- */
- willPatch() { }
- /**
- * This hook is called whenever a component did actually update its props,
- * state or env.
- *
- * This method is not called on the initial render. It is useful to interact
- * with the DOM (for example, through an external library) whenever the
- * component was updated.
- *
- * Updating the widget state in this hook is possible, but not encouraged.
- * One need to be careful, because updates here will cause rerender, which in
- * turn will cause other calls to updated. So, we need to be particularly
- * careful at avoiding endless cycles.
- *
- * The snapshot parameter is the result of the call to willPatch.
- */
- patched(snapshot) { }
- /**
- * willUnmount is a hook that is called each time just before a component is
- * unmounted from the DOM. This is a good place to remove some listeners, for
- * example.
- *
- * Note: this method should not be called manually.
- *
- * @see mounted
- */
- willUnmount() { }
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
- async mount(target) {
- const vnode = await this._prepare();
- if (this.__owl__.isDestroyed) {
- // widget was destroyed before we get here...
- return;
- }
- this._patch(vnode);
- target.appendChild(this.el);
- if (document.body.contains(target)) {
- this._callMounted();
- }
- }
- _callMounted() {
- const children = this.__owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (!comp.__owl__.isMounted && this.el.contains(comp.el)) {
- comp._callMounted();
- }
- }
- this.__owl__.isMounted = true;
- this.mounted();
- }
- _callWillUnmount() {
- this.willUnmount();
- this.__owl__.isMounted = false;
- const children = this.__owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (comp.__owl__.isMounted) {
- comp._callWillUnmount();
- }
- }
- }
- unmount() {
- if (this.__owl__.isMounted) {
- this._callWillUnmount();
- this.el.remove();
- }
- }
- async render(force = false, patchQueue) {
- if (!this.__owl__.isMounted) {
- return;
- }
- const shouldPatch = !patchQueue;
- if (shouldPatch) {
- patchQueue = [];
- }
- const renderVDom = this._render(force, patchQueue);
- const renderId = this.__owl__.renderId;
- await renderVDom;
- if (shouldPatch &&
- this.__owl__.isMounted &&
- renderId === this.__owl__.renderId) {
- // we only update the vnode and the actual DOM if no other rendering
- // occurred between now and when the render method was initially called.
- for (let i = 0; i < patchQueue.length; i++) {
- const patch = patchQueue[i];
- patch.push(patch[0].willPatch());
- }
- for (let i = 0; i < patchQueue.length; i++) {
- const patch = patchQueue[i];
- patch[0]._patch(patch[1]);
- }
- for (let i = patchQueue.length - 1; i >= 0; i--) {
- const patch = patchQueue[i];
- patch[0].patched(patch[2]);
- }
- }
- }
- destroy() {
- if (!this.__owl__.isDestroyed) {
- const el = this.el;
- this._destroy(this.__owl__.parent);
- if (el) {
- el.remove();
- }
- }
- }
- _destroy(parent) {
- const isMounted = this.__owl__.isMounted;
- if (isMounted) {
- this.willUnmount();
- this.__owl__.isMounted = false;
- }
- const children = Object.values(this.__owl__.children);
- for (let child of children) {
- child._destroy(this);
- }
- if (parent) {
- let id = this.__owl__.id;
- delete parent.__owl__.children[id];
- this.__owl__.parent = null;
- }
- this.clear();
- this.__owl__.isDestroyed = true;
- delete this.__owl__.vnode;
- }
- shouldUpdate(nextProps) {
- return true;
- }
- /**
- * This method is the correct way to update the environment of a widget. Doing
- * this will cause a full rerender of the widget and its children, so this is
- * an operation that should not be done frequently.
- *
- * A good usecase for updating the environment would be to update some mostly
- * static config keys, such as a boolean to determine if we are in mobile
- * mode or not.
- */
- async updateEnv(nextEnv) {
- if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
- this.env = Object.create(this.env);
- }
- Object.assign(this.env, nextEnv);
- if (this.__owl__.isMounted) {
- await this.render(true);
- }
- }
- set(target, key, value) {
- this.__owl__.observer.set(target, key, value);
- }
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- async _updateProps(nextProps, forceUpdate = false, patchQueue) {
- const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
- if (shouldUpdate) {
- await this.willUpdateProps(nextProps);
- this.props = nextProps;
- await this.render(forceUpdate, patchQueue);
- }
- }
- _patch(vnode) {
- this.__owl__.renderPromise = null;
- const target = this.__owl__.vnode || document.createElement(vnode.sel);
- this.__owl__.vnode = patch(target, vnode);
- }
- _prepare() {
- this.__owl__.renderProps = this.props;
- this.__owl__.renderPromise = this._prepareAndRender();
- return this.__owl__.renderPromise;
- }
- async _prepareAndRender() {
- await this.willStart();
- if (this.__owl__.isDestroyed) {
- return Promise.resolve(h("div"));
- }
- this.__owl__.isStarted = true;
- if (this.inlineTemplate) {
- this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
- }
- this.__owl__.render = this.env.qweb.render.bind(this.env.qweb, this.inlineTemplate || this.template);
- this._observeState();
- return this._render();
- }
- async _render(force = false, patchQueue = []) {
- this.__owl__.renderId++;
- const promises = [];
- const patch = [this];
- if (this.__owl__.isMounted) {
- patchQueue.push(patch);
- }
- if (this.__owl__.observer) {
- this.__owl__.observer.allowMutations = false;
- }
- let vnode = this.__owl__.render(this, {
- promises,
- handlers: this.__owl__.boundHandlers,
- forceUpdate: force,
- patchQueue
- });
- patch.push(vnode);
- if (this.__owl__.observer) {
- this.__owl__.observer.allowMutations = true;
- }
- // this part is critical for the patching process to be done correctly. The
- // tricky part is that a child widget can be rerendered on its own, which
- // will update its own vnode representation without the knowledge of the
- // parent widget. With this, we make sure that the parent widget will be
- // able to patch itself properly after
- vnode.key = this.__owl__.id;
- this.__owl__.renderProps = this.props;
- this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
- return this.__owl__.renderPromise;
- }
- /**
- * Only called by qweb t-widget directive
- */
- _mount(vnode, elm) {
- this.__owl__.vnode = patch(elm, vnode);
- if (this.__owl__.parent &&
- this.__owl__.parent.__owl__.isMounted &&
- !this.__owl__.isMounted) {
- this._callMounted();
- }
- return this.__owl__.vnode;
- }
- __mount() {
- if (!this.__owl__.isMounted) {
- this.__owl__.isMounted = true;
- this.mounted();
- }
- }
- _observeState() {
- if (this.state) {
- this.__owl__.observer = new Observer();
- this.__owl__.observer.observe(this.state);
- this.__owl__.observer.notifyCB = this.render.bind(this);
- }
- }
- }
-
- /**
- * Owl Utils
- *
- * We have here a small collection of utility functions:
- *
- * - escape
- * - memoize
- * - debounce
- * - patch
- * - unpatch
- * - loadTemplates
- * - loadJS
- * - whenReady
- * - parseXML
- */
- function escape(str) {
- if (str === undefined) {
- return "";
- }
- if (typeof str === "number") {
- return String(str);
- }
- return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, "'")
- .replace(/`/g, "`");
- }
- function memoize(f, hash) {
- if (!hash) {
- hash = args => args.map(a => String(a)).join(",");
- }
- let cache = {};
- function memoizedFunction(...args) {
- let hashValue = hash(args);
- if (!(hashValue in cache)) {
- cache[hashValue] = f(...args);
- }
- return cache[hashValue];
- }
- return memoizedFunction;
- }
- /**
- * Returns a function, that, as long as it continues to be invoked, will not
- * be triggered. The function will be called after it stops being called for
- * N milliseconds. If `immediate` is passed, trigger the function on the
- * leading edge, instead of the trailing.
- *
- * Inspired by https://davidwalsh.name/javascript-debounce-function
- */
- function debounce(func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- function later() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- }
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
- }
- function patch$1(C, patchName, patch) {
- const proto = C.prototype;
- if (!proto.__patches) {
- proto.__patches = {
- origMethods: {},
- patches: {},
- current: []
- };
- }
- if (proto.__patches.patches[patchName]) {
- throw new Error(`Patch [${patchName}] already exists`);
- }
- proto.__patches.patches[patchName] = patch;
- applyPatch(proto, patch);
- proto.__patches.current.push(patchName);
- function applyPatch(proto, patch) {
- Object.keys(patch).forEach(function (methodName) {
- const method = patch[methodName];
- if (typeof method === "function") {
- const original = proto[methodName];
- if (!(methodName in proto.__patches.origMethods)) {
- proto.__patches.origMethods[methodName] = original;
- }
- proto[methodName] = function (...args) {
- this._super = original;
- return method.call(this, ...args);
- };
- }
- });
- }
- }
- function unpatch(C, patchName) {
- const proto = C.prototype;
- const patchInfo = proto.__patches;
- delete proto.__patches;
- // reset to original
- for (let k in patchInfo.origMethods) {
- proto[k] = patchInfo.origMethods[k];
- }
- // apply other patches
- for (let name of patchInfo.current) {
- if (name !== patchName) {
- patch$1(C, name, patchInfo.patches[name]);
- }
- }
- }
- async function loadTemplates(url) {
- const result = await fetch(url);
- if (!result.ok) {
- throw new Error("Error while fetching xml templates");
- }
- let templates = await result.text();
- templates = templates.replace(//g, "");
- return templates;
- }
- const loadedScripts = {};
- function loadJS(url) {
- if (url in loadedScripts) {
- return loadedScripts[url];
- }
- const promise = new Promise(function (resolve, reject) {
- const script = document.createElement("script");
- script.type = "text/javascript";
- script.src = url;
- script.onload = function () {
- resolve();
- };
- script.onerror = function () {
- reject(`Error loading file '${url}'`);
- };
- const head = document.head || document.getElementsByTagName("head")[0];
- head.appendChild(script);
- });
- loadedScripts[url] = promise;
- return promise;
- }
- function whenReady(fn) {
- if (document.readyState === "complete") {
- fn();
- }
- else {
- document.addEventListener("DOMContentLoaded", fn, false);
- }
- }
- function parseXML(xml) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
- }
- return doc;
- }
-
- var _utils = /*#__PURE__*/Object.freeze({
- escape: escape,
- memoize: memoize,
- debounce: debounce,
- patch: patch$1,
- unpatch: unpatch,
- loadTemplates: loadTemplates,
- loadJS: loadJS,
- whenReady: whenReady,
- parseXML: parseXML
- });
-
- //------------------------------------------------------------------------------
- // Const/global stuff/helpers
- //------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const DISABLED_TAGS = [
- "input",
- "textarea",
- "button",
- "select",
- "option",
- "optgroup"
- ];
- const lineBreakRE = /[\r\n]/;
- const whitespaceRE = /\s+/g;
- const DIRECTIVE_NAMES = {
- name: 1,
- att: 1,
- attf: 1,
- key: 1
- };
- const DIRECTIVES = [];
- const UTILS = {
- h: h,
- objectToAttrString(obj) {
- let classes = [];
- for (let k in obj) {
- if (obj[k]) {
- classes.push(k);
- }
- }
- return classes.join(" ");
- }
- };
- //------------------------------------------------------------------------------
- // Compilation Context
- //------------------------------------------------------------------------------
- class Context {
- constructor(name) {
- this.nextID = 1;
- this.code = [];
- this.variables = {};
- this.definedVariables = {};
- this.escaping = false;
- this.parentNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldProtectContext = false;
- this.inLoop = false;
- this.inPreTag = false;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.addLine("var h = this.utils.h;");
- }
- generateID() {
- const id = this.rootContext.nextID++;
- return id;
- }
- withParent(node) {
- if (this === this.rootContext && this.parentNode) {
- throw new Error("A template should not have more than one root node");
- }
- if (!this.rootContext.rootNode) {
- this.rootContext.rootNode = node;
- }
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- formatExpression(e) {
- e = e.trim();
- if (e[0] === "{" && e[e.length - 1] === "}") {
- const innerExpr = e
- .slice(1, -1)
- .split(",")
- .map(p => {
- let [key, val] = p.trim().split(":");
- if (key === "") {
- return "";
- }
- if (!val) {
- val = key;
- }
- return `${key}: ${this.formatExpression(val)}`;
- })
- .join(",");
- return "{" + innerExpr + "}";
- }
- // Thanks CHM for this code...
- const chars = e.split("");
- let instring = "";
- let invar = "";
- let invarPos = 0;
- let r = "";
- chars.push(" ");
- for (var i = 0, ilen = chars.length; i < ilen; i++) {
- var c = chars[i];
- if (instring.length) {
- if (c === instring && chars[i - 1] !== "\\") {
- instring = "";
- }
- }
- else if (c === '"' || c === "'") {
- instring = c;
- }
- else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
- invar = c;
- invarPos = i;
- continue;
- }
- else if (c.match(/\W/) && invar.length) {
- // TODO: Should check for possible spaces before dot
- if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
- if (!(invar in this.definedVariables)) {
- invar =
- WORD_REPLACEMENT[invar] ||
- (invar in this.variables &&
- this.formatExpression(this.variables[invar])) ||
- "context['" + invar + "']";
- }
- }
- r += invar;
- invar = "";
- }
- else if (invar.length) {
- invar += c;
- continue;
- }
- r += c;
- }
- const result = r.slice(0, -1);
- return result;
- }
- }
- //------------------------------------------------------------------------------
- // QWeb rendering engine
- //------------------------------------------------------------------------------
- class QWeb {
- constructor(data) {
- this.templates = {};
- this.utils = UTILS;
- if (data) {
- this.loadTemplates(data);
- }
- }
- static addDirective(directive) {
- DIRECTIVES.push(directive);
- DIRECTIVE_NAMES[directive.name] = 1;
- DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
- if (directive.extraNames) {
- directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
- }
- }
- /**
- * Add a template to the internal template map. Note that it is not
- * immediately compiled.
- */
- addTemplate(name, xmlString, allowDuplicates = false) {
- if (name in this.templates && allowDuplicates) {
- return;
- }
- const doc = parseXML(xmlString);
- if (!doc.firstChild) {
- throw new Error("Invalid template (should not be empty)");
- }
- this._addTemplate(name, doc.firstChild);
- }
- _addTemplate(name, elem) {
- if (name in this.templates) {
- throw new Error(`Template ${name} already defined`);
- }
- this._processTemplate(elem);
- const template = {
- elem,
- fn: (context, extra) => {
- const compiledFunction = this._compile(name, elem);
- template.fn = compiledFunction;
- return compiledFunction.call(this, context, extra);
- }
- };
- this.templates[name] = template;
- }
- _processTemplate(elem) {
- let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
- let node = tbranch[i];
- let prevElem = node.previousElementSibling;
- let pattr = function (name) {
- return prevElem.getAttribute(name);
- };
- let nattr = function (name) {
- return +!!node.getAttribute(name);
- };
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
- if (pattr("t-foreach")) {
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
- }
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
- return a + b;
- }) > 1) {
- throw new Error("Only one conditional branching directive is allowed per node");
- }
- // All text nodes between branch nodes are removed
- let textNode;
- while ((textNode = node.previousSibling) !== prevElem) {
- if (textNode.nodeValue.trim().length) {
- throw new Error("text is not allowed between branching directives");
- }
- textNode.remove();
- }
- }
- else {
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
- }
- }
- }
- /**
- * Load templates from a xml (as a string). This will look up for the first
- * tag, and will consider each child of this as a template, with
- * the name given by the t-name attribute.
- */
- loadTemplates(xmlstr) {
- const doc = parseXML(xmlstr);
- const templates = doc.getElementsByTagName("templates")[0];
- if (!templates) {
- return;
- }
- for (let elem of templates.children) {
- const name = elem.getAttribute("t-name");
- this._addTemplate(name, elem);
- }
- }
- /**
- * Render a template
- *
- * @param {string} name the template should already have been added
- */
- render(name, context = {}, extra = null) {
- const template = this.templates[name];
- if (!template) {
- throw new Error(`Template ${name} does not exist`);
- }
- return template.fn.call(this, context, extra);
- }
- _compile(name, elem) {
- const isDebug = elem.attributes.hasOwnProperty("t-debug");
- const ctx = new Context(name);
- this._compileNode(elem, ctx);
- if (ctx.shouldProtectContext) {
- ctx.code.unshift(" context = Object.create(context);");
- }
- if (ctx.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- ctx.code.unshift(" let owner = context;");
- }
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
- }
- ctx.addLine(`return vn${ctx.rootNode};`);
- let template;
- try {
- template = new Function("context", "extra", ctx.code.join("\n"));
- }
- catch (e) {
- throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`);
- }
- if (isDebug) {
- console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
- template.toString());
- }
- return template;
- }
- /**
- * Generate code from an xml node
- *
- */
- _compileNode(node, ctx) {
- if (!(node instanceof Element)) {
- // this is a text node, there are no directive to apply
- let text = node.textContent;
- if (!ctx.inPreTag) {
- if (lineBreakRE.test(text) && !text.trim()) {
- return;
- }
- text = text.replace(whitespaceRE, " ");
- }
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
- }
- else {
- // this is an unusual situation: this text node is the result of the
- // template rendering.
- let nodeID = ctx.generateID();
- ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
- ctx.rootContext.rootNode = nodeID;
- ctx.rootContext.parentNode = nodeID;
- }
- return;
- }
- const attributes = node.attributes;
- const validDirectives = [];
- let withHandlers = false;
- // maybe this is not optimal: we iterate on all attributes here, and again
- // just after for each directive.
- for (let i = 0; i < attributes.length; i++) {
- let attrName = attributes[i].name;
- if (attrName.startsWith("t-")) {
- let dName = attrName.slice(2).split("-")[0];
- if (!(dName in DIRECTIVE_NAMES)) {
- throw new Error(`Unknown QWeb directive: '${attrName}'`);
- }
- }
- }
- for (let directive of DIRECTIVES) {
- let fullName;
- let value;
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- if (name === "t-" + directive.name ||
- name.startsWith("t-" + directive.name + "-")) {
- fullName = name;
- value = attributes[i].textContent;
- validDirectives.push({ directive, value, fullName });
- if (directive.name === "on") {
- withHandlers = true;
- }
- }
- }
- }
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeEncounter) {
- const isDone = directive.atNodeEncounter({
- node,
- qweb: this,
- ctx,
- fullName,
- value
- });
- if (isDone) {
- return;
- }
- }
- }
- if (node.nodeName !== "t") {
- let nodeID = this._compileGenericNode(node, ctx, withHandlers);
- ctx = ctx.withParent(nodeID);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeCreation) {
- directive.atNodeCreation({
- node,
- qweb: this,
- ctx,
- fullName,
- value,
- nodeID
- });
- }
- }
- }
- if (node.nodeName === "pre") {
- ctx = ctx.subContext("inPreTag", true);
- }
- this._compileChildren(node, ctx);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.finalize) {
- directive.finalize({ node, qweb: this, ctx, fullName, value });
- }
- }
- }
- _compileGenericNode(node, ctx, withHandlers = true) {
- // nodeType 1 is generic tag
- if (node.nodeType !== 1) {
- throw new Error("unsupported node type");
- }
- const attributes = node.attributes;
- const attrs = [];
- const props = [];
- const tattrs = [];
- function handleBooleanProps(key, val) {
- let isProp = false;
- if (node.nodeName === "input" && key === "checked") {
- let type = node.getAttribute("type");
- if (type === "checkbox" || type === "radio") {
- isProp = true;
- }
- }
- if (node.nodeName === "option" && key === "selected") {
- isProp = true;
- }
- if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
- isProp = true;
- }
- if ((key === "readonly" && node.nodeName === "input") ||
- node.nodeName === "textarea") {
- isProp = true;
- }
- if (isProp) {
- props.push(`${key}: _${val}`);
- }
- }
- for (let i = 0; i < attributes.length; i++) {
- let name = attributes[i].name;
- const value = attributes[i].textContent;
- // regular attributes
- if (!name.startsWith("t-") &&
- !node.getAttribute("t-attf-" + name)) {
- const attID = ctx.generateID();
- ctx.addLine(`var _${attID} = '${value}';`);
- if (!name.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- name = '"' + name + '"';
- }
- attrs.push(`${name}: _${attID}`);
- handleBooleanProps(name, attID);
- }
- // dynamic attributes
- if (name.startsWith("t-att-")) {
- let attName = name.slice(6);
- let formattedValue = ctx.formatExpression(ctx.getValue(value));
- if (formattedValue[0] === "{" &&
- formattedValue[formattedValue.length - 1] === "}") {
- formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
- }
- const attID = ctx.generateID();
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- // we need to combine dynamic with non dynamic attributes:
- // class="a" t-att-class="'yop'" should be rendered as class="a yop"
- const attValue = node.getAttribute(attName);
- if (attValue) {
- const attValueID = ctx.generateID();
- ctx.addLine(`var _${attValueID} = ${formattedValue};`);
- formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
- const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
- attrs.splice(attrIndex, 1);
- }
- ctx.addLine(`var _${attID} = ${formattedValue};`);
- attrs.push(`${attName}: _${attID}`);
- handleBooleanProps(attName, attID);
- }
- if (name.startsWith("t-attf-")) {
- let attName = name.slice(7);
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- const formattedExpr = value.replace(/\{\{.*?\}\}/g, s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}");
- const attID = ctx.generateID();
- let staticVal = node.getAttribute(attName);
- if (staticVal) {
- ctx.addLine(`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`);
- }
- else {
- ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
- }
- attrs.push(`${attName}: _${attID}`);
- }
- // t-att= attributes
- if (name === "t-att") {
- let id = ctx.generateID();
- ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`);
- tattrs.push(id);
- }
- }
- let nodeID = ctx.generateID();
- let nodeKey = node.getAttribute("t-key");
- if (nodeKey) {
- nodeKey = ctx.formatExpression(nodeKey);
- }
- else {
- nodeKey = nodeID;
- }
- const parts = [`key:${nodeKey}`];
- if (attrs.length + tattrs.length > 0) {
- parts.push(`attrs:{${attrs.join(",")}}`);
- }
- if (props.length > 0) {
- parts.push(`props:{${props.join(",")}}`);
- }
- if (withHandlers) {
- parts.push(`on:{}`);
- }
- ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
- for (let id of tattrs) {
- ctx.addIf(`_${id} instanceof Array`);
- ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
- ctx.addElse();
- ctx.addLine(`for (let key in _${id}) {`);
- ctx.indent();
- ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
- ctx.dedent();
- ctx.addLine(`}`);
- ctx.closeIf();
- }
- ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
- }
- return nodeID;
- }
- _compileChildren(node, ctx) {
- if (node.childNodes.length > 0) {
- for (let child of Array.from(node.childNodes)) {
- this._compileNode(child, ctx);
- }
- }
- }
- }
-
- /**
- * Owl QWeb Directives
- *
- * This file contains the implementation of most standard QWeb directives:
- * - t-esc
- * - t-raw
- * - t-set/t-value
- * - t-if/t-elif/t-else
- * - t-call
- * - t-foreach/t-as
- * - t-debug
- * - t-log
- */
- //------------------------------------------------------------------------------
- // t-esc and t-raw
- //------------------------------------------------------------------------------
- UTILS.getFragment = function (str) {
- const temp = document.createElement("template");
- temp.innerHTML = str;
- return temp.content;
- };
- function compileValueNode(value, node, qweb, ctx) {
- if (value === "0" && ctx.caller) {
- qweb._compileNode(ctx.caller, ctx);
- return;
- }
- if (typeof value === "string") {
- let exprID = value;
- if (!(value in ctx.definedVariables)) {
- exprID = `_${ctx.generateID()}`;
- ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
- }
- ctx.addIf(`${exprID} || ${exprID} === 0`);
- if (!ctx.parentNode) {
- throw new Error("Should not have a text node without a parent");
- }
- if (ctx.escaping) {
- ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
- }
- else {
- let fragID = ctx.generateID();
- ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
- let tempNodeID = ctx.generateID();
- ctx.addLine(`var p${tempNodeID} = {hook: {`);
- ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
- ctx.addLine(`}};`);
- ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
- ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
- }
- if (node.childNodes.length) {
- ctx.addElse();
- qweb._compileChildren(node, ctx);
- }
- ctx.closeIf();
- return;
- }
- if (value instanceof NodeList) {
- for (let node of Array.from(value)) {
- qweb._compileNode(node, ctx);
- }
- }
- }
- QWeb.addDirective({
- name: "esc",
- priority: 70,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-esc"));
- compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
- return true;
- }
- });
- QWeb.addDirective({
- name: "raw",
- priority: 80,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-raw"));
- compileValueNode(value, node, qweb, ctx);
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-set
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "set",
- extraNames: ["value"],
- priority: 60,
- atNodeEncounter({ node, ctx }) {
- const variable = node.getAttribute("t-set");
- let value = node.getAttribute("t-value");
- if (value) {
- const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable)) {
- ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`);
- }
- else {
- const varName = `_${ctx.generateID()}`;
- ctx.addLine(`var ${varName} = ${formattedValue};`);
- ctx.definedVariables[varName] = formattedValue;
- ctx.variables[variable] = varName;
- }
- }
- else {
- ctx.variables[variable] = node.childNodes;
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-if, t-elif, t-else
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "if",
- priority: 20,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-if"));
- ctx.addIf(`${ctx.formatExpression(cond)}`);
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- QWeb.addDirective({
- name: "elif",
- priority: 30,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-elif"));
- ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- QWeb.addDirective({
- name: "else",
- priority: 40,
- atNodeEncounter({ ctx }) {
- ctx.addLine(`else {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- //------------------------------------------------------------------------------
- // t-call
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "call",
- priority: 50,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- throw new Error("Invalid tag for t-call directive (should be 't')");
- }
- const subTemplate = node.getAttribute("t-call");
- const nodeTemplate = qweb.templates[subTemplate];
- if (!nodeTemplate) {
- throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
- }
- const nodeCopy = node.cloneNode(true);
- nodeCopy.removeAttribute("t-call");
- // extract variables from nodecopy
- const tempCtx = new Context();
- tempCtx.nextID = ctx.rootContext.nextID;
- qweb._compileNode(nodeCopy, tempCtx);
- const vars = Object.assign({}, ctx.variables, tempCtx.variables);
- var definedVariables = Object.assign({}, ctx.definedVariables, tempCtx.definedVariables);
- ctx.rootContext.nextID = tempCtx.nextID;
- // open new scope, if necessary
- const hasNewVariables = Object.keys(definedVariables).length > 0;
- if (hasNewVariables) {
- ctx.addLine("{");
- ctx.indent();
- }
- // add new variables, if any
- for (let key in definedVariables) {
- ctx.addLine(`let ${key} = ${definedVariables[key]}`);
- }
- // compile sub template
- const subCtx = ctx
- .subContext("caller", nodeCopy)
- .subContext("variables", Object.create(vars))
- .subContext("definedVariables", Object.create(definedVariables));
- qweb._compileNode(nodeTemplate.elem, subCtx);
- // close new scope
- if (hasNewVariables) {
- ctx.dedent();
- ctx.addLine("}");
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-foreach
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "foreach",
- extraNames: ["as"],
- priority: 10,
- atNodeEncounter({ node, qweb, ctx }) {
- ctx.rootContext.shouldProtectContext = true;
- ctx = ctx.subContext("inLoop", true);
- const elems = node.getAttribute("t-foreach");
- const name = node.getAttribute("t-as");
- let arrayID = ctx.generateID();
- ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
- ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
- ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`);
- let keysID = ctx.generateID();
- ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`);
- let valuesID = ctx.generateID();
- ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`);
- ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
- ctx.indent();
- ctx.addLine(`context.${name}_first = i === 0;`);
- ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`);
- ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
- ctx.addLine(`context.${name}_index = i;`);
- ctx.addLine(`context.${name} = _${keysID}[i];`);
- ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
- const nodeCopy = node.cloneNode(true);
- let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
- if (!shouldWarn && node.tagName === "t") {
- if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
- shouldWarn = true;
- }
- if (!shouldWarn &&
- node.children.length === 1 &&
- node.children[0].tagName !== "t" &&
- !node.children[0].hasAttribute("t-key")) {
- shouldWarn = true;
- }
- }
- if (shouldWarn) {
- console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`);
- }
- nodeCopy.removeAttribute("t-foreach");
- qweb._compileNode(nodeCopy, ctx);
- ctx.dedent();
- ctx.addLine("}");
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-debug
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "debug",
- priority: 99,
- atNodeEncounter({ ctx }) {
- ctx.addLine("debugger;");
- }
- });
- //------------------------------------------------------------------------------
- // t-log
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "log",
- priority: 99,
- atNodeEncounter({ ctx, value }) {
- const expr = ctx.formatExpression(value);
- ctx.addLine(`console.log(${expr})`);
- }
- });
-
- /**
- * Owl QWeb Extensions
- *
- * This file contains the implementation of non standard QWeb directives, added
- * by Owl and that will only work on Owl projects:
- *
- * - t-on
- * - t-ref
- * - t-transition
- * - t-widget/t-props/t-keepalive
- */
- //------------------------------------------------------------------------------
- // t-on
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "on",
- priority: 90,
- atNodeCreation({ ctx, fullName, value, nodeID }) {
- ctx.rootContext.shouldDefineOwner = true;
- const eventName = fullName.slice(5);
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handler = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`;
- if (extraArgs) {
- ctx.addLine(`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`);
- }
- else {
- ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`);
- ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
- }
- }
- });
- //------------------------------------------------------------------------------
- // t-ref
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "ref",
- priority: 95,
- atNodeCreation({ ctx, nodeID, value }) {
- const refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`);
- ctx.addLine(`p${nodeID}.hook = {
- create: (_, n) => context.refs[${refKey}] = n.elm,
- };`);
- }
- });
- //------------------------------------------------------------------------------
- // t-transition
- //------------------------------------------------------------------------------
- UTILS.nextFrame = function (cb) {
- requestAnimationFrame(() => requestAnimationFrame(cb));
- };
- UTILS.transitionCreate = function (elm, name) {
- elm.classList.add(name + "-enter");
- elm.classList.add(name + "-enter-active");
- };
- UTILS.transitionInsert = function (elm, name) {
- const finalize = () => {
- elm.classList.remove(name + "-enter-active");
- elm.classList.remove(name + "-enter-to");
- };
- this.nextFrame(() => {
- elm.classList.remove(name + "-enter");
- elm.classList.add(name + "-enter-to");
- whenTransitionEnd(elm, finalize);
- });
- };
- UTILS.transitionRemove = function (elm, name, rm) {
- elm.classList.add(name + "-leave");
- elm.classList.add(name + "-leave-active");
- const finalize = () => {
- elm.classList.remove(name + "-leave-active");
- elm.classList.remove(name + "-enter-to");
- rm();
- };
- this.nextFrame(() => {
- elm.classList.remove(name + "-leave");
- elm.classList.add(name + "-leave-to");
- whenTransitionEnd(elm, finalize);
- });
- };
- function getTimeout(delays, durations) {
- /* istanbul ignore next */
- while (delays.length < durations.length) {
- delays = delays.concat(delays);
- }
- return Math.max.apply(null, durations.map((d, i) => {
- return toMs(d) + toMs(delays[i]);
- }));
- }
- // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
- // in a locale-dependent way, using a comma instead of a dot.
- // If comma is not replaced with a dot, the input will be rounded down (i.e. acting
- // as a floor function) causing unexpected behaviors
- function toMs(s) {
- return Number(s.slice(0, -1).replace(",", ".")) * 1000;
- }
- function whenTransitionEnd(elm, cb) {
- const styles = window.getComputedStyle(elm);
- const delays = (styles.transitionDelay || "").split(", ");
- const durations = (styles.transitionDuration || "").split(", ");
- const timeout = getTimeout(delays, durations);
- if (timeout > 0) {
- elm.addEventListener("transitionend", cb, { once: true });
- }
- else {
- cb();
- }
- }
- QWeb.addDirective({
- name: "transition",
- priority: 96,
- atNodeCreation({ ctx, value, nodeID }) {
- let name = value;
- ctx.addLine(`p${nodeID}.hook = {
- create: (_, n) => {
- this.utils.transitionCreate(n.elm, '${name}');
- },
- insert: vn => {
- this.utils.transitionInsert(vn.elm, '${name}');
- },
- remove: (vn, rm) => {
- this.utils.transitionRemove(vn.elm, '${name}', rm);
- }
- };`);
- }
- });
- //------------------------------------------------------------------------------
- // t-widget
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "widget",
- extraNames: ["props", "keepalive"],
- priority: 100,
- atNodeEncounter({ ctx, value, node }) {
- ctx.addLine("//WIDGET");
- ctx.rootContext.shouldDefineOwner = true;
- let props = node.getAttribute("t-props");
- let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- // t-on- events...
- const events = [];
- const attributes = node.attributes;
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- if (name.startsWith("t-on-")) {
- events.push([name.slice(5), attributes[i].textContent]);
- }
- }
- let key = node.getAttribute("t-key");
- if (key) {
- key = ctx.formatExpression(key);
- }
- if (props) {
- props = ctx.formatExpression(props);
- }
- let dummyID = ctx.generateID();
- let defID = ctx.generateID();
- let widgetID = ctx.generateID();
- let keyID = key && ctx.generateID();
- if (key) {
- // we bind a variable to the key (could be a complex expression, so we
- // want to evaluate it only once)
- ctx.addLine(`let key${keyID} = ${key};`);
- }
- ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
- ctx.addLine(`c${ctx.parentNode}.push(null);`);
- ctx.addLine(`let def${defID};`);
- let templateID = key
- ? `key${keyID}`
- : ctx.inLoop
- ? `String(-${widgetID} - i)`
- : String(widgetID);
- ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`);
- ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
- ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
- // check if we can reuse current rendering promise
- ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`);
- ctx.addIf(`w${widgetID}.__owl__.isStarted`);
- ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`);
- ctx.addElse();
- ctx.addLine(`isNew${widgetID} = true`);
- ctx.addIf(`props${widgetID} === w${widgetID}.__owl__.renderProps`);
- ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
- ctx.addElse();
- ctx.addLine(`w${widgetID}.destroy();`);
- ctx.addLine(`w${widgetID} = false`);
- ctx.closeIf();
- ctx.closeIf();
- ctx.closeIf();
- ctx.addIf(`!def${defID}`);
- ctx.addIf(`w${widgetID}`);
- ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`);
- ctx.addElse();
- ctx.addLine(`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`);
- ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`);
- for (let [event, method] of events) {
- ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
- }
- ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
- ctx.closeIf();
- ctx.closeIf();
- let ref = node.getAttribute("t-ref");
- let refExpr = "";
- let refKey = "";
- if (ref) {
- refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`);
- refExpr = `context.refs[${refKey}] = w${widgetID};`;
- }
- let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}()`;
- if (ref) {
- finalizeWidgetCode += `;delete context.refs[${refKey}]`;
- }
- let createHook = "";
- let classAttr = node.getAttribute("class");
- let tattClass = node.getAttribute("t-att-class");
- let styleAttr = node.getAttribute("style");
- let tattStyle = node.getAttribute("t-att-style");
- if (tattStyle) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
- tattStyle = attVar;
- }
- let updateClassCode = "";
- if (classAttr || tattClass || styleAttr || tattStyle) {
- let classCode = "";
- if (classAttr) {
- classCode =
- classAttr
- .split(" ")
- .map(c => `vn.elm.classList.add('${c}')`)
- .join(";") + ";";
- }
- if (tattClass) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
- classCode = `for (let k in ${attVar}) {
- if (${attVar}[k]) {
- vn.elm.classList.add(k);
- }
- }`;
- updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
- }
- const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
- const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
- createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
- }
- ctx.addIf(`isNew${widgetID}`);
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}; w${widgetID}.__owl__.pvnode = pvnode;});`);
- ctx.addElse();
- ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){${finalizeWidgetCode}}, destroy() {${finalizeWidgetCode}}}}c${ctx.parentNode}[_${dummyID}_index]=vnode;});`);
- ctx.closeIf();
- ctx.addLine(`extra.promises.push(def${defID});`);
- if (node.hasAttribute("t-if") || node.hasAttribute("t-else")) {
- ctx.closeIf();
- }
- return true;
- }
- });
-
- class Store extends EventBus {
- constructor(config, options = {}) {
- super();
- this._commitLevel = 0;
- this.history = [];
- this.debug = options.debug || false;
- this.state = config.state || {};
- this.actions = config.actions;
- this.mutations = config.mutations;
- this.env = config.env;
- this.observer = new Observer();
- this.observer.notifyCB = this.trigger.bind(this, "update");
- this.observer.allowMutations = false;
- this.observer.observe(this.state);
- this.getters = {};
- if (this.debug) {
- this.history.push({ state: this.state });
- }
- this.set = this.observer.set.bind(this.observer);
- for (let entry of Object.entries(config.getters || {})) {
- const name = entry[0];
- const func = entry[1];
- this.getters[name] = payload => {
- return func({ state: this.state, getters: this.getters }, payload);
- };
- }
- }
- dispatch(action, payload) {
- if (!this.actions[action]) {
- throw new Error(`[Error] action ${action} is undefined`);
- }
- const result = this.actions[action]({
- commit: this.commit.bind(this),
- dispatch: this.dispatch.bind(this),
- env: this.env,
- state: this.state,
- getters: this.getters
- }, payload);
- if (result instanceof Promise) {
- return new Promise((resolve, reject) => {
- result.then(() => resolve());
- result.catch(reject);
- });
- }
- }
- commit(type, payload) {
- if (!this.mutations[type]) {
- throw new Error(`[Error] mutation ${type} is undefined`);
- }
- this._commitLevel++;
- this.observer.allowMutations = true;
- const res = this.mutations[type].call(null, {
- commit: this.commit.bind(this),
- state: this.state,
- set: this.set,
- getters: this.getters
- }, payload);
- if (this._commitLevel === 1) {
- this.observer.allowMutations = false;
- if (this.debug) {
- this.history.push({
- state: this.state,
- mutation: type,
- payload: payload
- });
- }
- }
- this._commitLevel--;
- return res;
- }
- }
- //------------------------------------------------------------------------------
- // Connect function
- //------------------------------------------------------------------------------
- function revNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.rev;
- }
- return 0;
- }
- function deepRevNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.deepRev;
- }
- return 0;
- }
- function connect(mapStateToProps, options = {}) {
- let hashFunction = options.hashFunction || null;
- if (!hashFunction) {
- let deep = "deep" in options ? options.deep : true;
- let defaultRevFunction = deep ? deepRevNumber : revNumber;
- hashFunction = function ({ storeProps }, options) {
- const { currentStoreProps } = options;
- if ("__owl__" in storeProps) {
- return defaultRevFunction(storeProps);
- }
- let hash = 0;
- for (let key in storeProps) {
- const val = storeProps[key];
- const hashVal = defaultRevFunction(val);
- if (hashVal === 0) {
- if (val !== currentStoreProps[key]) {
- options.didChange = true;
- }
- }
- else {
- hash += hashVal;
- }
- }
- return hash;
- };
- }
- return function (Comp) {
- return class extends Comp {
- constructor(parent, props) {
- const env = parent instanceof Component ? parent.env : parent;
- const ownProps = Object.assign({}, props || {});
- const storeProps = mapStateToProps(env.store.state, ownProps, env.store.getters);
- const mergedProps = Object.assign({}, props || {}, storeProps);
- super(parent, mergedProps);
- this.__owl__.ownProps = ownProps;
- this.__owl__.currentStoreProps = storeProps;
- this.__owl__.storeHash = hashFunction({
- state: env.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, {
- currentStoreProps: storeProps
- });
- }
- /**
- * We do not use the mounted hook here for a subtle reason: we want the
- * updates to be called for the parents before the children. However,
- * if we use the mounted hook, this will be done in the reverse order.
- */
- _callMounted() {
- this.env.store.on("update", this, this._checkUpdate);
- super._callMounted();
- }
- willUnmount() {
- this.env.store.off("update", this);
- super.willUnmount();
- }
- _checkUpdate() {
- const ownProps = this.__owl__.ownProps;
- const storeProps = mapStateToProps(this.env.store.state, ownProps, this.env.store.getters);
- const options = {
- currentStoreProps: this.__owl__.currentStoreProps
- };
- const storeHash = hashFunction({
- state: this.env.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, options);
- let didChange = options.didChange;
- if (storeHash !== this.__owl__.storeHash) {
- didChange = true;
- this.__owl__.storeHash = storeHash;
- }
- if (didChange) {
- this.__owl__.currentStoreProps = storeProps;
- this._updateProps(ownProps, false);
- }
- }
- _updateProps(nextProps, forceUpdate, patchQueue) {
- if (this.__owl__.ownProps !== nextProps) {
- this.__owl__.currentStoreProps = mapStateToProps(this.env.store.state, nextProps, this.env.store.getters);
- }
- this.__owl__.ownProps = nextProps;
- const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps);
- return super._updateProps(mergedProps, forceUpdate, patchQueue);
- }
- };
- };
- }
-
- /**
- * This file is the main file packaged by rollup (see rollup.config.js). From
- * this file, we export all public owl elements.
- *
- * Note that dynamic values, such as a date or a commit hash are added by rollup
- */
- const utils = _utils;
-
- exports.utils = utils;
- exports.Component = Component;
- exports.EventBus = EventBus;
- exports.Observer = Observer;
- exports.QWeb = QWeb;
- exports.connect = connect;
- exports.Store = Store;
-
- exports._version = '0.10.0';
- exports._date = '2019-05-10T21:54:47.545Z';
- exports._hash = 'e04c5b1';
- exports._url = 'https://github.com/odoo/owl';
-
-}(this.owl = this.owl || {}));
diff --git a/tools/benchmarks/owl-0.10.0/templates.xml b/tools/benchmarks/owl-0.10.0/templates.xml
deleted file mode 100644
index 699ebea3..00000000
--- a/tools/benchmarks/owl-0.10.0/templates.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/benchmarks/owl-0.12.0/app.js b/tools/benchmarks/owl-0.12.0/app.js
deleted file mode 100644
index 5410383e..00000000
--- a/tools/benchmarks/owl-0.12.0/app.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import {
- buildData,
- startMeasure,
- stopMeasure,
- formatNumber
-} from "../shared/utils.js";
-
-//------------------------------------------------------------------------------
-// Likes Counter Widget
-//------------------------------------------------------------------------------
-class Counter extends owl.Component {
- state = { counter: 0 };
-
- increment() {
- this.state.counter++;
- }
-}
-
-//------------------------------------------------------------------------------
-// Message Widget
-//------------------------------------------------------------------------------
-class Message extends owl.Component {
- widgets = { Counter };
-
- shouldUpdate(nextProps) {
- return nextProps !== this.props;
- }
- removeMessage() {
- this.trigger("remove_message", {
- id: this.props.id
- });
- }
-}
-
-//------------------------------------------------------------------------------
-// Root Widget
-//------------------------------------------------------------------------------
-class App extends owl.Component {
- widgets = { Message };
- state = { messages: [], multipleFlag: false, clearAfterFlag: false };
-
- mounted() {
- this.log(
- `Benchmarking Owl v${owl.__info__.version} (build date: ${
- owl.__info__.date
- })`
- );
- }
-
- benchmark(message, fn, callback) {
- if (this.state.multipleFlag) {
- const N = 20;
- let n = N;
- let total = 0;
- let cb = info => {
- let finalize = () => {
- n--;
- total += info.delta;
- if (n === 0) {
- const avg = total / N;
- this.log(`Average: ${formatNumber(avg)}ms`, true);
- if (callback) {
- callback();
- }
- } else {
- this._benchmark(message, fn, cb);
- }
- };
-
- if (this.state.clearAfterFlag) {
- this._benchmark(
- "clear",
- () => {
- this.state.messages = [];
- },
- finalize,
- false
- );
- } else {
- finalize();
- }
- };
- this._benchmark(message, fn, cb);
- } else {
- this._benchmark(message, fn, callback);
- }
- }
-
- _benchmark(message, fn, cb, log = true) {
- setTimeout(() => {
- startMeasure(message);
- fn();
- stopMeasure(info => {
- if (log) {
- this.log(info.msg);
- }
- if (cb) {
- cb(info);
- }
- });
- }, 10);
- }
-
- addMessages(n) {
- this.benchmark("add " + n, () => {
- const newMessages = buildData(n);
- this.state.messages.push.apply(this.state.messages, newMessages);
- });
- }
-
- clear() {
- this.benchmark("clear", () => {
- this.state.messages = [];
- });
- }
-
- updateSomeMessages() {
- this.benchmark("update every 10th", () => {
- const messages = this.state.messages;
- for (let i = 0; i < this.state.messages.length; i += 10) {
- const msg = Object.assign({}, messages[i]);
- msg.author += "!!!";
- this.set(messages, i, msg);
- }
- });
- }
-
- removeMessage(data) {
- this.benchmark("remove message", () => {
- const index = this.state.messages.findIndex(m => m.id === data.id);
- this.state.messages.splice(index, 1);
- });
- }
-
- log(str, isBold) {
- const div = document.createElement("div");
- if (isBold) {
- div.classList.add("bold");
- }
- div.textContent = `> ${str}`;
- this.refs.log.appendChild(div);
- this.refs.log.scrollTop = this.refs.log.scrollHeight;
- }
-
- clearLog() {
- this.refs.log.innerHTML = "";
- }
-
- toggleMultiple() {
- this.state.multipleFlag = !this.state.multipleFlag;
- }
-
- toggleClear() {
- this.state.clearAfterFlag = !this.state.clearAfterFlag;
- }
-}
-
-//------------------------------------------------------------------------------
-// Application initialization
-//------------------------------------------------------------------------------
-async function start() {
- const templates = await owl.utils.loadTemplates("templates.xml");
- const env = {
- qweb: new owl.QWeb(templates)
- };
- const app = new App(env);
- app.mount(document.body);
-}
-
-start();
diff --git a/tools/benchmarks/owl-0.12.0/owl.js b/tools/benchmarks/owl-0.12.0/owl.js
deleted file mode 100644
index 23514a17..00000000
--- a/tools/benchmarks/owl-0.12.0/owl.js
+++ /dev/null
@@ -1,3076 +0,0 @@
-(function (exports) {
- 'use strict';
-
- /**
- * We define here a simple event bus: it can
- * - emit events
- * - add/remove listeners.
- *
- * This is a useful pattern of communication in many cases. For OWL, each
- * components and stores are event buses.
- */
- //------------------------------------------------------------------------------
- // EventBus
- //------------------------------------------------------------------------------
- class EventBus {
- constructor() {
- this.subscriptions = {};
- }
- /**
- * Add a listener for the 'eventType' events.
- *
- * Note that the 'owner' of this event can be anything, but will more likely
- * be a widget or a class. The idea is that the callback will be called with
- * the proper owner bound.
- *
- * Also, the owner should be kind of unique. This will be used to remove the
- * listener.
- */
- on(eventType, owner, callback) {
- if (!callback) {
- throw new Error("Missing callback");
- }
- if (!this.subscriptions[eventType]) {
- this.subscriptions[eventType] = [];
- }
- this.subscriptions[eventType].push({
- owner,
- callback
- });
- }
- /**
- * Remove a listener
- */
- off(eventType, owner) {
- const subs = this.subscriptions[eventType];
- if (subs) {
- this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
- }
- }
- /**
- * Emit an event of type 'eventType'. Any extra arguments will be passed to
- * the listeners callback.
- */
- trigger(eventType, ...args) {
- const subs = this.subscriptions[eventType] || [];
- for (let i = 0, iLen = subs.length; i < iLen; i++) {
- const sub = subs[i];
- sub.callback.call(sub.owner, ...args);
- }
- }
- /**
- * Remove all subscriptions.
- */
- clear() {
- this.subscriptions = {};
- }
- }
-
- /**
- * Owl Observer
- *
- * This code contains the logic that allows Owl to observe and react to state
- * changes.
- *
- * This is a Observer class that can observe any JS values. The way it works
- * can be summarized thusly:
- * - primitive values are not observed at all
- * - Objects are observed by replacing all their keys with getters/setters
- * (recursively)
- * - Arrays are observed by replacing their prototype with a customized version,
- * which wrap some methods to allow the tracking of each state change.
- *
- * Note that this code is inspired by Vue.
- */
- //------------------------------------------------------------------------------
- // Modified Array prototype
- //------------------------------------------------------------------------------
- // we define here a new modified Array prototype, which basically override all
- // Array methods that change some state to be able to track their changes
- const methodsToPatch = [
- "push",
- "pop",
- "shift",
- "unshift",
- "splice",
- "sort",
- "reverse"
- ];
- const methodLen = methodsToPatch.length;
- const ArrayProto = Array.prototype;
- const ModifiedArrayProto = Object.create(ArrayProto);
- for (let i = 0; i < methodLen; i++) {
- const method = methodsToPatch[i];
- const initialMethod = ArrayProto[method];
- ModifiedArrayProto[method] = function (...args) {
- if (!this.__observer__.allowMutations) {
- throw new Error(`Array cannot be changed here")`);
- }
- this.__observer__.rev++;
- this.__observer__.notifyChange();
- this.__owl__.rev++;
- let parent = this;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent));
- let inserted;
- switch (method) {
- case "push":
- case "unshift":
- inserted = args;
- break;
- case "splice":
- inserted = args.slice(2);
- break;
- }
- if (inserted) {
- for (let i = 0, iLen = inserted.length; i < iLen; i++) {
- this.__observer__.observe(inserted[i], this);
- }
- }
- return initialMethod.call(this, ...args);
- };
- }
- //------------------------------------------------------------------------------
- // Observer
- //------------------------------------------------------------------------------
- class Observer {
- constructor() {
- this.rev = 1;
- this.allowMutations = true;
- this.dirty = false;
- }
- notifyCB() { }
- notifyChange() {
- this.dirty = true;
- Promise.resolve().then(() => {
- if (this.dirty) {
- this.dirty = false;
- this.notifyCB();
- }
- });
- }
- observe(value, parent) {
- if (value === null) {
- // fun fact: typeof null === 'object'
- return;
- }
- if (typeof value !== "object") {
- return;
- }
- if ("__owl__" in value) {
- // already observed
- value.__owl__.parent = parent;
- return;
- }
- if (Array.isArray(value)) {
- this._observeArr(value, parent);
- }
- else {
- this._observeObj(value, parent);
- }
- }
- set(target, key, value) {
- this.rev++;
- this._addProp(target, key, value);
- target.__owl__.rev++;
- this.notifyChange();
- }
- _observeObj(obj, parent) {
- obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
- Object.defineProperty(obj, "__owl__", { enumerable: false });
- for (let key in obj) {
- this._addProp(obj, key, obj[key]);
- }
- }
- _observeArr(arr, parent) {
- arr.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
- Object.defineProperty(arr, "__owl__", { enumerable: false });
- arr.__proto__ = Object.create(ModifiedArrayProto);
- arr.__proto__.__observer__ = this;
- for (let i = 0, iLen = arr.length; i < iLen; i++) {
- this.observe(arr[i], arr);
- }
- }
- _addProp(obj, key, value) {
- var self = this;
- Object.defineProperty(obj, key, {
- enumerable: true,
- get() {
- return value;
- },
- set(newVal) {
- if (newVal !== value) {
- self.rev++;
- if (!self.allowMutations) {
- throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`);
- }
- value = newVal;
- self.observe(newVal, obj);
- obj.__owl__.rev++;
- let parent = obj;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent) && parent !== obj);
- self.notifyChange();
- }
- }
- });
- this.observe(value, obj);
- }
- }
-
- /**
- * Owl VDOM
- *
- * This file contains an implementation of a virtual DOM, which is a system that
- * can generate in-memory representations of a DOM tree, compare them, and
- * eventually change a concrete DOM tree to match its representation, in an
- * hopefully efficient way.
- *
- * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our
- * needs (see https://github.com/snabbdom/snabbdom).
- *
- * The main exported values are:
- * - interface VNode
- * - h function (a helper function to generate a vnode)
- * - patch function (to apply a vnode to an actual DOM node)
- */
- function vnode(sel, data, children, text, elm) {
- let key = data === undefined ? undefined : data.key;
- return {
- sel: sel,
- data: data,
- children: children,
- text: text,
- elm: elm,
- key: key
- };
- }
- //------------------------------------------------------------------------------
- // snabbdom.ts
- //------------------------------------------------------------------------------
- function isUndef(s) {
- return s === undefined;
- }
- function isDef(s) {
- return s !== undefined;
- }
- const emptyNode = vnode("", {}, [], undefined, undefined);
- function sameVnode(vnode1, vnode2) {
- return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
- }
- function isVnode(vnode) {
- return vnode.sel !== undefined;
- }
- function createKeyToOldIdx(children, beginIdx, endIdx) {
- let i, map = {}, key, ch;
- for (i = beginIdx; i <= endIdx; ++i) {
- ch = children[i];
- if (ch != null) {
- key = ch.key;
- if (key !== undefined)
- map[key] = i;
- }
- }
- return map;
- }
- const hooks = [
- "create",
- "update",
- "remove",
- "destroy",
- "pre",
- "post"
- ];
- function init(modules, domApi) {
- let i, j, cbs = {};
- const api = domApi !== undefined ? domApi : htmlDomApi;
- for (i = 0; i < hooks.length; ++i) {
- cbs[hooks[i]] = [];
- for (j = 0; j < modules.length; ++j) {
- const hook = modules[j][hooks[i]];
- if (hook !== undefined) {
- cbs[hooks[i]].push(hook);
- }
- }
- }
- function emptyNodeAt(elm) {
- const id = elm.id ? "#" + elm.id : "";
- const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
- return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
- }
- function createRmCb(childElm, listeners) {
- return function rmCb() {
- if (--listeners === 0) {
- const parent = api.parentNode(childElm);
- api.removeChild(parent, childElm);
- }
- };
- }
- function createElm(vnode, insertedVnodeQueue) {
- let i, iLen, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.init))) {
- i(vnode);
- data = vnode.data;
- }
- }
- let children = vnode.children, sel = vnode.sel;
- if (sel === "!") {
- if (isUndef(vnode.text)) {
- vnode.text = "";
- }
- vnode.elm = api.createComment(vnode.text);
- }
- else if (sel !== undefined) {
- // Parse selector
- const hashIdx = sel.indexOf("#");
- const dotIdx = sel.indexOf(".", hashIdx);
- const hash = hashIdx > 0 ? hashIdx : sel.length;
- const dot = dotIdx > 0 ? dotIdx : sel.length;
- const tag = hashIdx !== -1 || dotIdx !== -1
- ? sel.slice(0, Math.min(hash, dot))
- : sel;
- const elm = (vnode.elm =
- isDef(data) && isDef((i = data.ns))
- ? api.createElementNS(i, tag)
- : api.createElement(tag));
- if (hash < dot)
- elm.setAttribute("id", sel.slice(hash + 1, dot));
- if (dotIdx > 0)
- elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
- for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
- cbs.create[i](emptyNode, vnode);
- if (array(children)) {
- for (i = 0, iLen = children.length; i < iLen; ++i) {
- const ch = children[i];
- if (ch != null) {
- api.appendChild(elm, createElm(ch, insertedVnodeQueue));
- }
- }
- }
- else if (primitive(vnode.text)) {
- api.appendChild(elm, api.createTextNode(vnode.text));
- }
- i = vnode.data.hook; // Reuse variable
- if (isDef(i)) {
- if (i.create)
- i.create(emptyNode, vnode);
- if (i.insert)
- insertedVnodeQueue.push(vnode);
- }
- }
- else {
- vnode.elm = api.createTextNode(vnode.text);
- }
- return vnode.elm;
- }
- function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
- for (; startIdx <= endIdx; ++startIdx) {
- const ch = vnodes[startIdx];
- if (ch != null) {
- api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
- }
- }
- }
- function invokeDestroyHook(vnode) {
- let i, iLen, j, jLen, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.destroy)))
- i(vnode);
- for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
- cbs.destroy[i](vnode);
- if (vnode.children !== undefined) {
- for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
- i = vnode.children[j];
- if (i != null && typeof i !== "string") {
- invokeDestroyHook(i);
- }
- }
- }
- }
- }
- function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
- for (; startIdx <= endIdx; ++startIdx) {
- let i, iLen, listeners, rm, ch = vnodes[startIdx];
- if (ch != null) {
- if (isDef(ch.sel)) {
- invokeDestroyHook(ch);
- listeners = cbs.remove.length + 1;
- rm = createRmCb(ch.elm, listeners);
- for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
- cbs.remove[i](ch, rm);
- if (isDef((i = ch.data)) &&
- isDef((i = i.hook)) &&
- isDef((i = i.remove))) {
- i(ch, rm);
- }
- else {
- rm();
- }
- }
- else {
- // Text node
- api.removeChild(parentElm, ch.elm);
- }
- }
- }
- }
- function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
- let oldStartIdx = 0, newStartIdx = 0;
- let oldEndIdx = oldCh.length - 1;
- let oldStartVnode = oldCh[0];
- let oldEndVnode = oldCh[oldEndIdx];
- let newEndIdx = newCh.length - 1;
- let newStartVnode = newCh[0];
- let newEndVnode = newCh[newEndIdx];
- let oldKeyToIdx;
- let idxInOld;
- let elmToMove;
- let before;
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
- if (oldStartVnode == null) {
- oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
- }
- else if (oldEndVnode == null) {
- oldEndVnode = oldCh[--oldEndIdx];
- }
- else if (newStartVnode == null) {
- newStartVnode = newCh[++newStartIdx];
- }
- else if (newEndVnode == null) {
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newStartVnode)) {
- patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
- oldStartVnode = oldCh[++oldStartIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else if (sameVnode(oldEndVnode, newEndVnode)) {
- patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
- oldEndVnode = oldCh[--oldEndIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newEndVnode)) {
- // Vnode moved right
- patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
- oldStartVnode = oldCh[++oldStartIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldEndVnode, newStartVnode)) {
- // Vnode moved left
- patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode = oldCh[--oldEndIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- if (oldKeyToIdx === undefined) {
- oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
- }
- idxInOld = oldKeyToIdx[newStartVnode.key];
- if (isUndef(idxInOld)) {
- // New element
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- elmToMove = oldCh[idxInOld];
- if (elmToMove.sel !== newStartVnode.sel) {
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- }
- else {
- patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
- oldCh[idxInOld] = undefined;
- api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
- }
- newStartVnode = newCh[++newStartIdx];
- }
- }
- }
- if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
- if (oldStartIdx > oldEndIdx) {
- before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
- addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
- }
- else {
- removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
- }
- }
- }
- function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
- let i, iLen, hook;
- if (isDef((i = vnode.data)) &&
- isDef((hook = i.hook)) &&
- isDef((i = hook.prepatch))) {
- i(oldVnode, vnode);
- }
- const elm = (vnode.elm = oldVnode.elm);
- let oldCh = oldVnode.children;
- let ch = vnode.children;
- if (oldVnode === vnode)
- return;
- if (vnode.data !== undefined) {
- for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
- cbs.update[i](oldVnode, vnode);
- i = vnode.data.hook;
- if (isDef(i) && isDef((i = i.update)))
- i(oldVnode, vnode);
- }
- if (isUndef(vnode.text)) {
- if (isDef(oldCh) && isDef(ch)) {
- if (oldCh !== ch)
- updateChildren(elm, oldCh, ch, insertedVnodeQueue);
- }
- else if (isDef(ch)) {
- if (isDef(oldVnode.text))
- api.setTextContent(elm, "");
- addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
- }
- else if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- else if (isDef(oldVnode.text)) {
- api.setTextContent(elm, "");
- }
- }
- else if (oldVnode.text !== vnode.text) {
- if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- api.setTextContent(elm, vnode.text);
- }
- if (isDef(hook) && isDef((i = hook.postpatch))) {
- i(oldVnode, vnode);
- }
- }
- return function patch(oldVnode, vnode) {
- let i, iLen, elm, parent;
- const insertedVnodeQueue = [];
- for (i = 0, iLen = cbs.pre.length; i < iLen; ++i)
- cbs.pre[i]();
- if (!isVnode(oldVnode)) {
- oldVnode = emptyNodeAt(oldVnode);
- }
- if (sameVnode(oldVnode, vnode)) {
- patchVnode(oldVnode, vnode, insertedVnodeQueue);
- }
- else {
- elm = oldVnode.elm;
- parent = api.parentNode(elm);
- createElm(vnode, insertedVnodeQueue);
- if (parent !== null) {
- api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
- removeVnodes(parent, [oldVnode], 0, 0);
- }
- }
- for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
- insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
- }
- for (i = 0, iLen = cbs.post.length; i < iLen; ++i)
- cbs.post[i]();
- return vnode;
- };
- }
- //------------------------------------------------------------------------------
- // is.ts
- //------------------------------------------------------------------------------
- const array = Array.isArray;
- function primitive(s) {
- return typeof s === "string" || typeof s === "number";
- }
- function createElement(tagName) {
- return document.createElement(tagName);
- }
- function createElementNS(namespaceURI, qualifiedName) {
- return document.createElementNS(namespaceURI, qualifiedName);
- }
- function createTextNode(text) {
- return document.createTextNode(text);
- }
- function createComment(text) {
- return document.createComment(text);
- }
- function insertBefore(parentNode, newNode, referenceNode) {
- parentNode.insertBefore(newNode, referenceNode);
- }
- function removeChild(node, child) {
- node.removeChild(child);
- }
- function appendChild(node, child) {
- node.appendChild(child);
- }
- function parentNode(node) {
- return node.parentNode;
- }
- function nextSibling(node) {
- return node.nextSibling;
- }
- function tagName(elm) {
- return elm.tagName;
- }
- function setTextContent(node, text) {
- node.textContent = text;
- }
- function getTextContent(node) {
- return node.textContent;
- }
- function isElement(node) {
- return node.nodeType === 1;
- }
- function isText(node) {
- return node.nodeType === 3;
- }
- function isComment(node) {
- return node.nodeType === 8;
- }
- const htmlDomApi = {
- createElement,
- createElementNS,
- createTextNode,
- createComment,
- insertBefore,
- removeChild,
- appendChild,
- parentNode,
- nextSibling,
- tagName,
- setTextContent,
- getTextContent,
- isElement,
- isText,
- isComment
- };
- function addNS(data, children, sel) {
- data.ns = "http://www.w3.org/2000/svg";
- if (sel !== "foreignObject" && children !== undefined) {
- for (let i = 0, iLen = children.length; i < iLen; ++i) {
- let childData = children[i].data;
- if (childData !== undefined) {
- addNS(childData, children[i].children, children[i].sel);
- }
- }
- }
- }
- function h(sel, b, c) {
- var data = {}, children, text, i, iLen;
- if (c !== undefined) {
- data = b;
- if (array(c)) {
- children = c;
- }
- else if (primitive(c)) {
- text = c;
- }
- else if (c && c.sel) {
- children = [c];
- }
- }
- else if (b !== undefined) {
- if (array(b)) {
- children = b;
- }
- else if (primitive(b)) {
- text = b;
- }
- else if (b && b.sel) {
- children = [b];
- }
- else {
- data = b;
- }
- }
- if (children !== undefined) {
- for (i = 0, iLen = children.length; i < iLen; ++i) {
- if (primitive(children[i]))
- children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
- }
- }
- if (sel[0] === "s" &&
- sel[1] === "v" &&
- sel[2] === "g" &&
- (sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
- addNS(data, children, sel);
- }
- return vnode(sel, data, children, text, undefined);
- }
- function updateProps(oldVnode, vnode) {
- var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
- if (!oldProps && !props)
- return;
- if (oldProps === props)
- return;
- oldProps = oldProps || {};
- props = props || {};
- for (key in oldProps) {
- if (!props[key]) {
- delete elm[key];
- }
- }
- for (key in props) {
- cur = props[key];
- old = oldProps[key];
- if (old !== cur && (key !== "value" || elm[key] !== cur)) {
- elm[key] = cur;
- }
- }
- }
- const propsModule = {
- create: updateProps,
- update: updateProps
- };
- function invokeHandler(handler, vnode, event) {
- if (typeof handler === "function") {
- // call function handler
- handler.call(vnode, event, vnode);
- }
- else if (typeof handler === "object") {
- // call handler with arguments
- if (typeof handler[0] === "function") {
- // special case for single argument for performance
- if (handler.length === 2) {
- handler[0].call(vnode, handler[1], event, vnode);
- }
- else {
- var args = handler.slice(1);
- args.push(event);
- args.push(vnode);
- handler[0].apply(vnode, args);
- }
- }
- else {
- // call multiple handlers
- for (let i = 0, iLen = handler.length; i < iLen; i++) {
- invokeHandler(handler[i], vnode, event);
- }
- }
- }
- }
- function handleEvent(event, vnode) {
- var name = event.type, on = vnode.data.on;
- // call event handler(s) if exists
- if (on && on[name]) {
- invokeHandler(on[name], vnode, event);
- }
- }
- function createListener() {
- return function handler(event) {
- handleEvent(event, handler.vnode);
- };
- }
- function updateEventListeners(oldVnode, vnode) {
- var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name;
- // optimization for reused immutable handlers
- if (oldOn === on) {
- return;
- }
- // remove existing listeners which no longer used
- if (oldOn && oldListener) {
- // if element changed or deleted we remove all existing listeners unconditionally
- if (!on) {
- for (name in oldOn) {
- // remove listener if element was changed or existing listeners removed
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- else {
- for (name in oldOn) {
- // remove listener if existing listener removed
- if (!on[name]) {
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- }
- }
- // add new listeners which has not already attached
- if (on) {
- // reuse existing listener or create new
- var listener = (vnode.listener =
- oldVnode.listener || createListener());
- // update vnode for listener
- listener.vnode = vnode;
- // if element changed or added we add all needed listeners unconditionally
- if (!oldOn) {
- for (name in on) {
- // add listener if element was changed or new listeners added
- elm.addEventListener(name, listener, false);
- }
- }
- else {
- for (name in on) {
- // add listener if new listener added
- if (!oldOn[name]) {
- elm.addEventListener(name, listener, false);
- }
- }
- }
- }
- }
- const eventListenersModule = {
- create: updateEventListeners,
- update: updateEventListeners,
- destroy: updateEventListeners
- };
- const xlinkNS = "http://www.w3.org/1999/xlink";
- const xmlNS = "http://www.w3.org/XML/1998/namespace";
- const colonChar = 58;
- const xChar = 120;
- function updateAttrs(oldVnode, vnode) {
- var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs;
- if (!oldAttrs && !attrs)
- return;
- if (oldAttrs === attrs)
- return;
- oldAttrs = oldAttrs || {};
- attrs = attrs || {};
- // update modified attributes, add new attributes
- for (key in attrs) {
- const cur = attrs[key];
- const old = oldAttrs[key];
- if (old !== cur) {
- if (cur === true) {
- elm.setAttribute(key, "");
- }
- else if (cur === false) {
- elm.removeAttribute(key);
- }
- else {
- if (key.charCodeAt(0) !== xChar) {
- elm.setAttribute(key, cur);
- }
- else if (key.charCodeAt(3) === colonChar) {
- // Assume xml namespace
- elm.setAttributeNS(xmlNS, key, cur);
- }
- else if (key.charCodeAt(5) === colonChar) {
- // Assume xlink namespace
- elm.setAttributeNS(xlinkNS, key, cur);
- }
- else {
- elm.setAttribute(key, cur);
- }
- }
- }
- }
- // remove removed attributes
- // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
- // the other option is to remove all attributes with value == undefined
- for (key in oldAttrs) {
- if (!(key in attrs)) {
- elm.removeAttribute(key);
- }
- }
- }
- const attrsModule = {
- create: updateAttrs,
- update: updateAttrs
- };
- const patch = init([eventListenersModule, attrsModule, propsModule]);
-
- //------------------------------------------------------------------------------
- // Const/global stuff/helpers
- //------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const DISABLED_TAGS = [
- "input",
- "textarea",
- "button",
- "select",
- "option",
- "optgroup"
- ];
- const lineBreakRE = /[\r\n]/;
- const whitespaceRE = /\s+/g;
- const DIRECTIVE_NAMES = {
- name: 1,
- att: 1,
- attf: 1,
- key: 1
- };
- const DIRECTIVES = [];
- const NODE_HOOKS_PARAMS = {
- create: "(_, n)",
- insert: "vn",
- remove: "(vn, rm)"
- };
- const UTILS = {
- h: h,
- objectToAttrString(obj) {
- let classes = [];
- for (let k in obj) {
- if (obj[k]) {
- classes.push(k);
- }
- }
- return classes.join(" ");
- }
- };
- function parseXML(xml) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
- }
- return doc;
- }
- let nextID = 1;
- //------------------------------------------------------------------------------
- // QWeb rendering engine
- //------------------------------------------------------------------------------
- class QWeb {
- constructor(data) {
- this.templates = {};
- this.utils = UTILS;
- // the id field is useful to be able to hash qweb instances. The current
- // use case is that component's templates are qweb dependant, and need to be
- // able to map a qweb instance to a template name.
- this.id = nextID++;
- if (data) {
- this.addTemplates(data);
- }
- this.addTemplate("default", "");
- }
- static addDirective(directive) {
- DIRECTIVES.push(directive);
- DIRECTIVE_NAMES[directive.name] = 1;
- DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
- if (directive.extraNames) {
- directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
- }
- }
- static register(name, Component) {
- if (QWeb.widgets[name]) {
- throw new Error(`Component '${name}' has already been registered`);
- }
- QWeb.widgets[name] = Component;
- }
- /**
- * Add a template to the internal template map. Note that it is not
- * immediately compiled.
- */
- addTemplate(name, xmlString) {
- const doc = parseXML(xmlString);
- if (!doc.firstChild) {
- throw new Error("Invalid template (should not be empty)");
- }
- this._addTemplate(name, doc.firstChild);
- }
- /**
- * Load templates from a xml (as a string). This will look up for the first
- * tag, and will consider each child of this as a template, with
- * the name given by the t-name attribute.
- */
- addTemplates(xmlstr) {
- const doc = parseXML(xmlstr);
- const templates = doc.getElementsByTagName("templates")[0];
- if (!templates) {
- return;
- }
- for (let elem of templates.children) {
- const name = elem.getAttribute("t-name");
- this._addTemplate(name, elem);
- }
- }
- _addTemplate(name, elem) {
- if (name in this.templates) {
- throw new Error(`Template ${name} already defined`);
- }
- this._processTemplate(elem);
- const template = {
- elem,
- fn: (context, extra) => {
- const compiledFunction = this._compile(name, elem);
- template.fn = compiledFunction;
- return compiledFunction.call(this, context, extra);
- }
- };
- this.templates[name] = template;
- }
- _processTemplate(elem) {
- let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
- let node = tbranch[i];
- let prevElem = node.previousElementSibling;
- let pattr = function (name) {
- return prevElem.getAttribute(name);
- };
- let nattr = function (name) {
- return +!!node.getAttribute(name);
- };
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
- if (pattr("t-foreach")) {
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
- }
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
- return a + b;
- }) > 1) {
- throw new Error("Only one conditional branching directive is allowed per node");
- }
- // All text nodes between branch nodes are removed
- let textNode;
- while ((textNode = node.previousSibling) !== prevElem) {
- if (textNode.nodeValue.trim().length) {
- throw new Error("text is not allowed between branching directives");
- }
- textNode.remove();
- }
- }
- else {
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
- }
- }
- }
- /**
- * Render a template
- *
- * @param {string} name the template should already have been added
- */
- render(name, context = {}, extra = null) {
- const template = this.templates[name];
- if (!template) {
- throw new Error(`Template ${name} does not exist`);
- }
- return template.fn.call(this, context, extra);
- }
- _compile(name, elem) {
- const isDebug = elem.attributes.hasOwnProperty("t-debug");
- const ctx = new Context(name);
- this._compileNode(elem, ctx);
- if (ctx.shouldProtectContext) {
- ctx.code.unshift(" context = Object.create(context);");
- }
- if (ctx.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- ctx.code.unshift(" let owner = context;");
- }
- if (ctx.shouldDefineQWeb) {
- ctx.code.unshift(" let QWeb = this.constructor;");
- }
- if (ctx.shouldDefineUtils) {
- ctx.code.unshift(" let utils = this.utils;");
- }
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
- }
- ctx.addLine(`return vn${ctx.rootNode};`);
- let template;
- try {
- template = new Function("context", "extra", ctx.code.join("\n"));
- }
- catch (e) {
- throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`);
- }
- if (isDebug) {
- console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
- template.toString());
- }
- return template;
- }
- /**
- * Generate code from an xml node
- *
- */
- _compileNode(node, ctx) {
- if (!(node instanceof Element)) {
- // this is a text node, there are no directive to apply
- let text = node.textContent;
- if (!ctx.inPreTag) {
- if (lineBreakRE.test(text) && !text.trim()) {
- return;
- }
- text = text.replace(whitespaceRE, " ");
- }
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
- }
- else {
- // this is an unusual situation: this text node is the result of the
- // template rendering.
- let nodeID = ctx.generateID();
- ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
- ctx.rootContext.rootNode = nodeID;
- ctx.rootContext.parentNode = nodeID;
- }
- return;
- }
- const attributes = node.attributes;
- const validDirectives = [];
- let withHandlers = false;
- // maybe this is not optimal: we iterate on all attributes here, and again
- // just after for each directive.
- for (let i = 0; i < attributes.length; i++) {
- let attrName = attributes[i].name;
- if (attrName.startsWith("t-")) {
- let dName = attrName.slice(2).split("-")[0];
- if (!(dName in DIRECTIVE_NAMES)) {
- throw new Error(`Unknown QWeb directive: '${attrName}'`);
- }
- }
- }
- const DIR_N = DIRECTIVES.length;
- const ATTR_N = attributes.length;
- for (let i = 0; i < DIR_N; i++) {
- let directive = DIRECTIVES[i];
- let fullName;
- let value;
- for (let j = 0; j < ATTR_N; j++) {
- const name = attributes[j].name;
- if (name === "t-" + directive.name ||
- name.startsWith("t-" + directive.name + "-")) {
- fullName = name;
- value = attributes[j].textContent;
- validDirectives.push({ directive, value, fullName });
- if (directive.name === "on") {
- withHandlers = true;
- }
- }
- }
- }
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeEncounter) {
- const isDone = directive.atNodeEncounter({
- node,
- qweb: this,
- ctx,
- fullName,
- value
- });
- if (isDone) {
- return;
- }
- }
- }
- if (node.nodeName !== "t") {
- let nodeID = this._compileGenericNode(node, ctx, withHandlers);
- ctx = ctx.withParent(nodeID);
- let nodeHooks = {};
- let addNodeHook = function (hook, handler) {
- nodeHooks[hook] = nodeHooks[hook] || [];
- nodeHooks[hook].push(handler);
- };
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeCreation) {
- directive.atNodeCreation({
- node,
- qweb: this,
- ctx,
- fullName,
- value,
- nodeID,
- addNodeHook
- });
- }
- }
- if (Object.keys(nodeHooks).length) {
- ctx.addLine(`p${nodeID}.hook = {`);
- for (let hook in nodeHooks) {
- ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
- for (let handler of nodeHooks[hook]) {
- ctx.addLine(` ${handler}`);
- }
- ctx.addLine(` },`);
- }
- ctx.addLine(`};`);
- }
- }
- if (node.nodeName === "pre") {
- ctx = ctx.subContext("inPreTag", true);
- }
- this._compileChildren(node, ctx);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.finalize) {
- directive.finalize({ node, qweb: this, ctx, fullName, value });
- }
- }
- }
- _compileGenericNode(node, ctx, withHandlers = true) {
- // nodeType 1 is generic tag
- if (node.nodeType !== 1) {
- throw new Error("unsupported node type");
- }
- const attributes = node.attributes;
- const attrs = [];
- const props = [];
- const tattrs = [];
- function handleBooleanProps(key, val) {
- let isProp = false;
- if (node.nodeName === "input" && key === "checked") {
- let type = node.getAttribute("type");
- if (type === "checkbox" || type === "radio") {
- isProp = true;
- }
- }
- if (node.nodeName === "option" && key === "selected") {
- isProp = true;
- }
- if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
- isProp = true;
- }
- if ((key === "readonly" && node.nodeName === "input") ||
- node.nodeName === "textarea") {
- isProp = true;
- }
- if (isProp) {
- props.push(`${key}: _${val}`);
- }
- }
- for (let i = 0; i < attributes.length; i++) {
- let name = attributes[i].name;
- const value = attributes[i].textContent;
- // regular attributes
- if (!name.startsWith("t-") &&
- !node.getAttribute("t-attf-" + name)) {
- const attID = ctx.generateID();
- ctx.addLine(`var _${attID} = '${value}';`);
- if (!name.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- name = '"' + name + '"';
- }
- attrs.push(`${name}: _${attID}`);
- handleBooleanProps(name, attID);
- }
- // dynamic attributes
- if (name.startsWith("t-att-")) {
- let attName = name.slice(6);
- let formattedValue = ctx.formatExpression(ctx.getValue(value));
- if (formattedValue[0] === "{" &&
- formattedValue[formattedValue.length - 1] === "}") {
- formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
- }
- const attID = ctx.generateID();
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- // we need to combine dynamic with non dynamic attributes:
- // class="a" t-att-class="'yop'" should be rendered as class="a yop"
- const attValue = node.getAttribute(attName);
- if (attValue) {
- const attValueID = ctx.generateID();
- ctx.addLine(`var _${attValueID} = ${formattedValue};`);
- formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
- const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
- attrs.splice(attrIndex, 1);
- }
- ctx.addLine(`var _${attID} = ${formattedValue};`);
- attrs.push(`${attName}: _${attID}`);
- handleBooleanProps(attName, attID);
- }
- if (name.startsWith("t-attf-")) {
- let attName = name.slice(7);
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- const formattedExpr = ctx.interpolate(value);
- const attID = ctx.generateID();
- let staticVal = node.getAttribute(attName);
- if (staticVal) {
- ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
- }
- else {
- ctx.addLine(`var _${attID} = ${formattedExpr};`);
- }
- attrs.push(`${attName}: _${attID}`);
- }
- // t-att= attributes
- if (name === "t-att") {
- let id = ctx.generateID();
- ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`);
- tattrs.push(id);
- }
- }
- let nodeID = ctx.generateID();
- let nodeKey = node.getAttribute("t-key");
- if (nodeKey) {
- nodeKey = ctx.formatExpression(nodeKey);
- }
- else {
- nodeKey = nodeID;
- }
- const parts = [`key:${nodeKey}`];
- if (attrs.length + tattrs.length > 0) {
- parts.push(`attrs:{${attrs.join(",")}}`);
- }
- if (props.length > 0) {
- parts.push(`props:{${props.join(",")}}`);
- }
- if (withHandlers) {
- parts.push(`on:{}`);
- }
- ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
- for (let id of tattrs) {
- ctx.addIf(`_${id} instanceof Array`);
- ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
- ctx.addElse();
- ctx.addLine(`for (let key in _${id}) {`);
- ctx.indent();
- ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
- ctx.dedent();
- ctx.addLine(`}`);
- ctx.closeIf();
- }
- ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
- }
- return nodeID;
- }
- _compileChildren(node, ctx) {
- if (node.childNodes.length > 0) {
- for (let child of Array.from(node.childNodes)) {
- this._compileNode(child, ctx);
- }
- }
- }
- }
- QWeb.widgets = Object.create(null);
- // dev mode enables better error messages or more costly validations
- QWeb.dev = false;
- //------------------------------------------------------------------------------
- // Compilation Context
- //------------------------------------------------------------------------------
- class Context {
- constructor(name) {
- this.nextID = 1;
- this.code = [];
- this.variables = {};
- this.definedVariables = {};
- this.escaping = false;
- this.parentNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldDefineQWeb = false;
- this.shouldDefineUtils = false;
- this.shouldProtectContext = false;
- this.inLoop = false;
- this.inPreTag = false;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.addLine("var h = this.utils.h;");
- }
- generateID() {
- const id = this.rootContext.nextID++;
- return id;
- }
- withParent(node) {
- if (this === this.rootContext && this.parentNode) {
- throw new Error("A template should not have more than one root node");
- }
- if (!this.rootContext.rootNode) {
- this.rootContext.rootNode = node;
- }
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- formatExpression(e) {
- e = e.trim();
- if (e[0] === "{" && e[e.length - 1] === "}") {
- const innerExpr = e
- .slice(1, -1)
- .split(",")
- .map(p => {
- let [key, val] = p.trim().split(":");
- if (key === "") {
- return "";
- }
- if (!val) {
- val = key;
- }
- return `${key}: ${this.formatExpression(val)}`;
- })
- .join(",");
- return "{" + innerExpr + "}";
- }
- // Thanks CHM for this code...
- const chars = e.split("");
- let instring = "";
- let invar = "";
- let invarPos = 0;
- let r = "";
- chars.push(" ");
- for (let i = 0, ilen = chars.length; i < ilen; i++) {
- let c = chars[i];
- if (instring.length) {
- if (c === instring && chars[i - 1] !== "\\") {
- instring = "";
- }
- }
- else if (c === '"' || c === "'") {
- instring = c;
- }
- else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
- invar = c;
- invarPos = i;
- continue;
- }
- else if (c.match(/\W/) && invar.length) {
- // TODO: Should check for possible spaces before dot
- if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
- if (!(invar in this.definedVariables)) {
- invar =
- WORD_REPLACEMENT[invar] ||
- (invar in this.variables &&
- this.formatExpression(this.variables[invar])) ||
- "context['" + invar + "']";
- }
- }
- r += invar;
- invar = "";
- }
- else if (invar.length) {
- invar += c;
- continue;
- }
- r += c;
- }
- const result = r.slice(0, -1);
- return result;
- }
- /**
- * Perform string interpolation on the given string. Note that if the whole
- * string is an expression, it simply returns it (formatted).
- * For instance:
- * 'Hello {{x}}!' -> `Hello ${x}`
- * '{{x}}' -> x
- */
- interpolate(s) {
- let matches = s.match(/\{\{.*?\}\}/g);
- if (matches && matches[0].length === s.length) {
- return this.formatExpression(s.slice(2, -2));
- }
- matches = s.match(/\#\{.*?\}/g);
- if (matches && matches[0].length === s.length) {
- return this.formatExpression(s.slice(2, -1));
- }
- let formatter = expr => "${" + this.formatExpression(expr) + "}";
- let r = s
- .replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
- .replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
- return "`" + r + "`";
- }
- }
-
- // If a component does not define explicitely a template
- // key, it needs to find a template with its name (or a parent's). This is
- // qweb dependant, so we need a place to store this information indexed by
- // qweb instances.
- const TEMPLATE_MAP = {};
- //------------------------------------------------------------------------------
- // Widget
- //------------------------------------------------------------------------------
- let nextId = 1;
- class Component extends EventBus {
- //--------------------------------------------------------------------------
- // Lifecycle
- //--------------------------------------------------------------------------
- /**
- * Creates an instance of Component.
- *
- * The root widget of a component tree needs an environment:
- *
- * ```javascript
- * const root = new RootWidget(env, props);
- * ```
- *
- * Every other widget simply needs a reference to its parent:
- *
- * ```javascript
- * const child = new SomeWidget(parent, props);
- * ```
- *
- * Note that most of the time, only the root widget needs to be created by
- * hand. Other widgets should be created automatically by the framework (with
- * the t-widget directive in a template)
- */
- constructor(parent, props) {
- super();
- this.refs = {};
- const defaultProps = this.constructor.defaultProps;
- if (defaultProps) {
- props = this._applyDefaultProps(props, defaultProps);
- }
- if (QWeb.dev) {
- this._validateProps(props || {});
- }
- // is this a good idea?
- // Pro: if props is empty, we can create easily a widget
- // Con: this is not really safe
- // Pro: but creating widget (by a template) is always unsafe anyway
- this.props = props || {};
- let id = nextId++;
- let p = null;
- if (parent instanceof Component) {
- p = parent;
- this.env = parent.env;
- parent.__owl__.children[id] = this;
- }
- else {
- this.env = parent;
- }
- this.__owl__ = {
- id: id,
- vnode: null,
- isMounted: false,
- isDestroyed: false,
- parent: p,
- children: {},
- cmap: {},
- renderId: 1,
- renderPromise: null,
- renderProps: props || null,
- boundHandlers: {},
- mountedHandlers: {}
- };
- }
- get el() {
- return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
- }
- /**
- * willStart is an asynchronous hook that can be implemented to perform some
- * action before the initial rendering of a component.
- *
- * It will be called exactly once before the initial rendering. It is useful
- * in some cases, for example, to load external assets (such as a JS library)
- * before the widget is rendered.
- *
- * Note that a slow willStart method will slow down the rendering of the user
- * interface. Therefore, some effort should be made to make this method as
- * fast as possible.
- *
- * Note: this method should not be called manually.
- */
- async willStart() { }
- /**
- * mounted is a hook that is called each time a component is attached to the
- * DOM. This is a good place to add some listeners, or to interact with the
- * DOM, if the component needs to perform some measure for example.
- *
- * Note: this method should not be called manually.
- *
- * @see willUnmount
- */
- mounted() { }
- /**
- * The willUpdateProps is an asynchronous hook, called just before new props
- * are set. This is useful if the component needs some asynchronous task
- * performed, depending on the props (for example, assuming that the props are
- * some record Id, fetching the record data).
- *
- * This hook is not called during the first render (but willStart is called
- * and performs a similar job).
- */
- async willUpdateProps(nextProps) { }
- /**
- * The willPatch hook is called just before the DOM patching process starts.
- * It is not called on the initial render. This is useful to get some
- * information which are in the DOM. For example, the current position of the
- * scrollbar
- *
- * The return value of willPatch will be given to the patched function.
- */
- willPatch() { }
- /**
- * This hook is called whenever a component did actually update its props,
- * state or env.
- *
- * This method is not called on the initial render. It is useful to interact
- * with the DOM (for example, through an external library) whenever the
- * component was updated.
- *
- * Updating the widget state in this hook is possible, but not encouraged.
- * One need to be careful, because updates here will cause rerender, which in
- * turn will cause other calls to updated. So, we need to be particularly
- * careful at avoiding endless cycles.
- *
- * The snapshot parameter is the result of the call to willPatch.
- */
- patched(snapshot) { }
- /**
- * willUnmount is a hook that is called each time just before a component is
- * unmounted from the DOM. This is a good place to remove some listeners, for
- * example.
- *
- * Note: this method should not be called manually.
- *
- * @see mounted
- */
- willUnmount() { }
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
- async mount(target) {
- const vnode = await this._prepare();
- if (this.__owl__.isDestroyed) {
- // widget was destroyed before we get here...
- return;
- }
- this._patch(vnode);
- target.appendChild(this.el);
- if (document.body.contains(target)) {
- this._callMounted();
- }
- }
- _callMounted() {
- const __owl__ = this.__owl__;
- const children = __owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (!comp.__owl__.isMounted && this.el.contains(comp.el)) {
- comp._callMounted();
- }
- }
- __owl__.isMounted = true;
- const handlers = __owl__.mountedHandlers;
- for (let key in handlers) {
- handlers[key]();
- }
- this.mounted();
- }
- _callWillUnmount() {
- this.willUnmount();
- const __owl__ = this.__owl__;
- __owl__.isMounted = false;
- const children = __owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (comp.__owl__.isMounted) {
- comp._callWillUnmount();
- }
- }
- }
- unmount() {
- if (this.__owl__.isMounted) {
- this._callWillUnmount();
- this.el.remove();
- }
- }
- async render(force = false, patchQueue) {
- const __owl__ = this.__owl__;
- if (!__owl__.isMounted) {
- return;
- }
- const shouldPatch = !patchQueue;
- if (shouldPatch) {
- patchQueue = [];
- }
- const renderVDom = this._render(force, patchQueue);
- const renderId = __owl__.renderId;
- await renderVDom;
- if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
- // we only update the vnode and the actual DOM if no other rendering
- // occurred between now and when the render method was initially called.
- const patchLen = patchQueue.length;
- for (let i = 0; i < patchLen; i++) {
- const patch = patchQueue[i];
- patch.push(patch[0].willPatch());
- }
- for (let i = 0; i < patchLen; i++) {
- const patch = patchQueue[i];
- patch[0]._patch(patch[1]);
- }
- for (let i = patchLen - 1; i >= 0; i--) {
- const patch = patchQueue[i];
- patch[0].patched(patch[2]);
- }
- }
- }
- destroy() {
- const __owl__ = this.__owl__;
- if (!__owl__.isDestroyed) {
- const el = this.el;
- this._destroy(__owl__.parent);
- if (el) {
- el.remove();
- }
- }
- }
- _destroy(parent) {
- const __owl__ = this.__owl__;
- const isMounted = __owl__.isMounted;
- if (isMounted) {
- this.willUnmount();
- __owl__.isMounted = false;
- }
- const children = __owl__.children;
- for (let key in children) {
- children[key]._destroy(this);
- }
- if (parent) {
- let id = __owl__.id;
- delete parent.__owl__.children[id];
- __owl__.parent = null;
- }
- this.clear();
- __owl__.isDestroyed = true;
- delete __owl__.vnode;
- }
- shouldUpdate(nextProps) {
- return true;
- }
- /**
- * This method is the correct way to update the environment of a widget. Doing
- * this will cause a full rerender of the widget and its children, so this is
- * an operation that should not be done frequently.
- *
- * A good usecase for updating the environment would be to update some mostly
- * static config keys, such as a boolean to determine if we are in mobile
- * mode or not.
- */
- async updateEnv(nextEnv) {
- const __owl__ = this.__owl__;
- if (__owl__.parent && __owl__.parent.env === this.env) {
- this.env = Object.create(this.env);
- }
- Object.assign(this.env, nextEnv);
- if (__owl__.isMounted) {
- await this.render(true);
- }
- }
- set(target, key, value) {
- this.__owl__.observer.set(target, key, value);
- }
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- async _updateProps(nextProps, forceUpdate = false, patchQueue) {
- const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
- if (shouldUpdate) {
- const defaultProps = this.constructor.defaultProps;
- if (defaultProps) {
- nextProps = this._applyDefaultProps(nextProps, defaultProps);
- }
- if (QWeb.dev) {
- this._validateProps(nextProps);
- }
- await this.willUpdateProps(nextProps);
- this.props = nextProps;
- await this.render(forceUpdate, patchQueue);
- }
- }
- _patch(vnode) {
- const __owl__ = this.__owl__;
- __owl__.renderPromise = null;
- const target = __owl__.vnode || document.createElement(vnode.sel);
- __owl__.vnode = patch(target, vnode);
- }
- _prepare() {
- const __owl__ = this.__owl__;
- __owl__.renderProps = this.props;
- __owl__.renderPromise = this._prepareAndRender();
- return __owl__.renderPromise;
- }
- async _prepareAndRender() {
- await this.willStart();
- const __owl__ = this.__owl__;
- if (__owl__.isDestroyed) {
- return Promise.resolve(h("div"));
- }
- const qweb = this.env.qweb;
- if (!this.template) {
- let tmap = TEMPLATE_MAP[qweb.id];
- if (!tmap) {
- tmap = {};
- TEMPLATE_MAP[qweb.id] = tmap;
- }
- let p = this.constructor;
- let name = p.name;
- let template = tmap[name];
- if (template) {
- this.template = template;
- }
- else {
- while ((template = p.name) &&
- !(template in qweb.templates) &&
- p !== Component) {
- p = p.__proto__;
- }
- if (p === Component) {
- this.template = "default";
- }
- else {
- tmap[name] = template;
- this.template = template;
- }
- }
- }
- __owl__.render = qweb.render.bind(qweb, this.template);
- this._observeState();
- return this._render();
- }
- async _render(force = false, patchQueue = []) {
- const __owl__ = this.__owl__;
- __owl__.renderId++;
- const promises = [];
- const patch = [this];
- if (__owl__.isMounted) {
- patchQueue.push(patch);
- }
- if (__owl__.observer) {
- __owl__.observer.allowMutations = false;
- }
- let vnode = __owl__.render(this, {
- promises,
- handlers: __owl__.boundHandlers,
- mountedHandlers: __owl__.mountedHandlers,
- forceUpdate: force,
- patchQueue
- });
- patch.push(vnode);
- if (__owl__.observer) {
- __owl__.observer.allowMutations = true;
- }
- // this part is critical for the patching process to be done correctly. The
- // tricky part is that a child widget can be rerendered on its own, which
- // will update its own vnode representation without the knowledge of the
- // parent widget. With this, we make sure that the parent widget will be
- // able to patch itself properly after
- vnode.key = __owl__.id;
- __owl__.renderProps = this.props;
- __owl__.renderPromise = Promise.all(promises).then(() => vnode);
- return __owl__.renderPromise;
- }
- /**
- * Only called by qweb t-widget directive
- */
- _mount(vnode, elm) {
- const __owl__ = this.__owl__;
- __owl__.vnode = patch(elm, vnode);
- if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) {
- this._callMounted();
- }
- return __owl__.vnode;
- }
- /**
- * Only called by qweb t-widget directive (when t-keepalive is set)
- */
- _remount() {
- const __owl__ = this.__owl__;
- if (!__owl__.isMounted) {
- __owl__.isMounted = true;
- this.mounted();
- }
- }
- _observeState() {
- if (this.state) {
- const __owl__ = this.__owl__;
- __owl__.observer = new Observer();
- __owl__.observer.observe(this.state);
- __owl__.observer.notifyCB = this.render.bind(this);
- }
- }
- /**
- * Apply default props (only top level).
- *
- * Note that this method does not modify in place the props, it returns a new
- * prop object
- */
- _applyDefaultProps(props, defaultProps) {
- props = props ? Object.create(props) : {};
- for (let propName in defaultProps) {
- if (props[propName] === undefined) {
- props[propName] = defaultProps[propName];
- }
- }
- return props;
- }
- /**
- * Validate the component props (or next props) against the (static) props
- * description. This is potentially an expensive operation: it may needs to
- * visit recursively the props and all the children to check if they are valid.
- * This is why it is only done in 'dev' mode.
- */
- _validateProps(props) {
- const propsDef = this.constructor.props;
- if (propsDef instanceof Array) {
- // list of strings (prop names)
- for (let i = 0, l = propsDef.length; i < l; i++) {
- if (!(propsDef[i] in props)) {
- throw new Error(`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`);
- }
- }
- }
- else if (propsDef) {
- // propsDef is an object now
- for (let propName in propsDef) {
- if (!(propName in props)) {
- if (propsDef[propName] && !propsDef[propName].optional) {
- throw new Error(`Missing props '${propName}' (widget '${this.constructor.name}')`);
- }
- else {
- break;
- }
- }
- let isValid = isValidProp(props[propName], propsDef[propName]);
- if (!isValid) {
- throw new Error(`Props '${propName}' of invalid type in widget '${this.constructor.name}'`);
- }
- }
- }
- }
- }
- //------------------------------------------------------------------------------
- // Prop validation helper
- //------------------------------------------------------------------------------
- /**
- * Check if an invidual prop value matches its (static) prop definition
- */
- function isValidProp(prop, propDef) {
- if (typeof propDef === "function") {
- // Check if a value is constructed by some Constructor. Note that there is a
- // slight abuse of language: we want to consider primitive values as well.
- //
- // So, even though 1 is not an instance of Number, we want to consider that
- // it is valid.
- if (typeof prop === "object") {
- return prop instanceof propDef;
- }
- return typeof prop === propDef.name.toLowerCase();
- }
- else if (propDef instanceof Array) {
- // If this code is executed, this means that we want to check if a prop
- // matches at least one of its descriptor.
- let result = false;
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
- result = result || isValidProp(prop, propDef[i]);
- }
- return result;
- }
- // propsDef is an object
- let result = isValidProp(prop, propDef.type);
- if (propDef.type === Array) {
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
- result = result && isValidProp(prop[i], propDef.element);
- }
- }
- if (propDef.type === Object) {
- const shape = propDef.shape;
- for (let key in shape) {
- result = result && isValidProp(prop[key], shape[key]);
- }
- }
- return result;
- }
-
- /**
- * Owl QWeb Directives
- *
- * This file contains the implementation of most standard QWeb directives:
- * - t-esc
- * - t-raw
- * - t-set/t-value
- * - t-if/t-elif/t-else
- * - t-call
- * - t-foreach/t-as
- * - t-debug
- * - t-log
- */
- //------------------------------------------------------------------------------
- // t-esc and t-raw
- //------------------------------------------------------------------------------
- UTILS.getFragment = function (str) {
- const temp = document.createElement("template");
- temp.innerHTML = str;
- return temp.content;
- };
- function compileValueNode(value, node, qweb, ctx) {
- if (value === "0" && ctx.caller) {
- qweb._compileNode(ctx.caller, ctx);
- return;
- }
- if (typeof value === "string") {
- let exprID = value;
- if (!(value in ctx.definedVariables)) {
- exprID = `_${ctx.generateID()}`;
- ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
- }
- ctx.addIf(`${exprID} || ${exprID} === 0`);
- if (!ctx.parentNode) {
- throw new Error("Should not have a text node without a parent");
- }
- if (ctx.escaping) {
- ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
- }
- else {
- let fragID = ctx.generateID();
- ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
- let tempNodeID = ctx.generateID();
- ctx.addLine(`var p${tempNodeID} = {hook: {`);
- ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
- ctx.addLine(`}};`);
- ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
- ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
- }
- if (node.childNodes.length) {
- ctx.addElse();
- qweb._compileChildren(node, ctx);
- }
- ctx.closeIf();
- return;
- }
- if (value instanceof NodeList) {
- for (let node of Array.from(value)) {
- qweb._compileNode(node, ctx);
- }
- }
- }
- QWeb.addDirective({
- name: "esc",
- priority: 70,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-esc"));
- compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
- return true;
- }
- });
- QWeb.addDirective({
- name: "raw",
- priority: 80,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-raw"));
- compileValueNode(value, node, qweb, ctx);
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-set
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "set",
- extraNames: ["value"],
- priority: 60,
- atNodeEncounter({ node, ctx }) {
- const variable = node.getAttribute("t-set");
- let value = node.getAttribute("t-value");
- if (value) {
- const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable)) {
- ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`);
- }
- else {
- const varName = `_${ctx.generateID()}`;
- ctx.addLine(`var ${varName} = ${formattedValue};`);
- ctx.definedVariables[varName] = formattedValue;
- ctx.variables[variable] = varName;
- }
- }
- else {
- ctx.variables[variable] = node.childNodes;
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-if, t-elif, t-else
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "if",
- priority: 20,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-if"));
- ctx.addIf(`${ctx.formatExpression(cond)}`);
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- QWeb.addDirective({
- name: "elif",
- priority: 30,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-elif"));
- ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- QWeb.addDirective({
- name: "else",
- priority: 40,
- atNodeEncounter({ ctx }) {
- ctx.addLine(`else {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- //------------------------------------------------------------------------------
- // t-call
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "call",
- priority: 50,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- throw new Error("Invalid tag for t-call directive (should be 't')");
- }
- const subTemplate = node.getAttribute("t-call");
- const nodeTemplate = qweb.templates[subTemplate];
- if (!nodeTemplate) {
- throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
- }
- const nodeCopy = node.cloneNode(true);
- nodeCopy.removeAttribute("t-call");
- // extract variables from nodecopy
- const tempCtx = new Context();
- tempCtx.nextID = ctx.rootContext.nextID;
- qweb._compileNode(nodeCopy, tempCtx);
- const vars = Object.assign({}, ctx.variables, tempCtx.variables);
- var definedVariables = Object.assign({}, ctx.definedVariables, tempCtx.definedVariables);
- ctx.rootContext.nextID = tempCtx.nextID;
- // open new scope, if necessary
- const hasNewVariables = Object.keys(definedVariables).length > 0;
- if (hasNewVariables) {
- ctx.addLine("{");
- ctx.indent();
- }
- // add new variables, if any
- for (let key in definedVariables) {
- ctx.addLine(`let ${key} = ${definedVariables[key]}`);
- }
- // compile sub template
- const subCtx = ctx
- .subContext("caller", nodeCopy)
- .subContext("variables", Object.create(vars))
- .subContext("definedVariables", Object.create(definedVariables));
- qweb._compileNode(nodeTemplate.elem, subCtx);
- // close new scope
- if (hasNewVariables) {
- ctx.dedent();
- ctx.addLine("}");
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-foreach
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "foreach",
- extraNames: ["as"],
- priority: 10,
- atNodeEncounter({ node, qweb, ctx }) {
- ctx.rootContext.shouldProtectContext = true;
- ctx = ctx.subContext("inLoop", true);
- const elems = node.getAttribute("t-foreach");
- const name = node.getAttribute("t-as");
- let arrayID = ctx.generateID();
- ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
- ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
- ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`);
- let keysID = ctx.generateID();
- ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`);
- ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
- let valuesID = ctx.generateID();
- ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`);
- ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
- ctx.indent();
- ctx.addLine(`context.${name}_first = i === 0;`);
- ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
- ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
- ctx.addLine(`context.${name}_index = i;`);
- ctx.addLine(`context.${name} = _${keysID}[i];`);
- ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
- const nodeCopy = node.cloneNode(true);
- let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
- if (!shouldWarn && node.tagName === "t") {
- if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
- shouldWarn = true;
- }
- if (!shouldWarn &&
- node.children.length === 1 &&
- node.children[0].tagName !== "t" &&
- !node.children[0].hasAttribute("t-key")) {
- shouldWarn = true;
- }
- }
- if (shouldWarn) {
- console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`);
- }
- nodeCopy.removeAttribute("t-foreach");
- qweb._compileNode(nodeCopy, ctx);
- ctx.dedent();
- ctx.addLine("}");
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-debug
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "debug",
- priority: 99,
- atNodeEncounter({ ctx }) {
- ctx.addLine("debugger;");
- }
- });
- //------------------------------------------------------------------------------
- // t-log
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "log",
- priority: 99,
- atNodeEncounter({ ctx, value }) {
- const expr = ctx.formatExpression(value);
- ctx.addLine(`console.log(${expr})`);
- }
- });
-
- /**
- * Owl QWeb Extensions
- *
- * This file contains the implementation of non standard QWeb directives, added
- * by Owl and that will only work on Owl projects:
- *
- * - t-on
- * - t-ref
- * - t-transition
- * - t-widget/t-props/t-keepalive
- * - t-mounted
- */
- //------------------------------------------------------------------------------
- // t-on
- //------------------------------------------------------------------------------
- // these are pieces of code that will be injected into the event handler if
- // modifiers are specified
- const MODS_CODE = {
- prevent: "e.preventDefault();",
- self: "if (e.target !== this.elm) {return}",
- stop: "e.stopPropagation();"
- };
- QWeb.addDirective({
- name: "on",
- priority: 90,
- atNodeCreation({ ctx, fullName, value, nodeID }) {
- ctx.rootContext.shouldDefineOwner = true;
- const [eventName, ...mods] = fullName.slice(5).split(".");
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handlerName = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- ctx.addIf(`!context['${handlerName}']`);
- ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`);
- ctx.closeIf();
- let params = extraArgs
- ? `owner, ${ctx.formatExpression(extraArgs)}`
- : "owner";
- let handler;
- if (mods.length > 0) {
- handler = `function (e) {`;
- handler += mods
- .map(function (mod) {
- return MODS_CODE[mod];
- })
- .join("");
- handler += `context['${handlerName}'].call(${params}, e);}`;
- }
- else {
- handler = `context['${handlerName}'].bind(${params})`;
- }
- if (extraArgs) {
- ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
- }
- else {
- ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`);
- ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
- }
- }
- });
- //------------------------------------------------------------------------------
- // t-ref
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "ref",
- priority: 95,
- atNodeCreation({ ctx, value, addNodeHook }) {
- const refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
- addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
- }
- });
- //------------------------------------------------------------------------------
- // t-transition
- //------------------------------------------------------------------------------
- UTILS.nextFrame = function (cb) {
- requestAnimationFrame(() => requestAnimationFrame(cb));
- };
- UTILS.transitionInsert = function (elm, name) {
- elm.classList.add(name + "-enter");
- elm.classList.add(name + "-enter-active");
- const finalize = () => {
- elm.classList.remove(name + "-enter-active");
- elm.classList.remove(name + "-enter-to");
- };
- this.nextFrame(() => {
- elm.classList.remove(name + "-enter");
- elm.classList.add(name + "-enter-to");
- whenTransitionEnd(elm, finalize);
- });
- };
- UTILS.transitionRemove = function (elm, name, rm) {
- elm.classList.add(name + "-leave");
- elm.classList.add(name + "-leave-active");
- const finalize = () => {
- elm.classList.remove(name + "-leave-active");
- elm.classList.remove(name + "-leave-to");
- rm();
- };
- this.nextFrame(() => {
- elm.classList.remove(name + "-leave");
- elm.classList.add(name + "-leave-to");
- whenTransitionEnd(elm, finalize);
- });
- };
- function getTimeout(delays, durations) {
- /* istanbul ignore next */
- while (delays.length < durations.length) {
- delays = delays.concat(delays);
- }
- return Math.max.apply(null, durations.map((d, i) => {
- return toMs(d) + toMs(delays[i]);
- }));
- }
- // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
- // in a locale-dependent way, using a comma instead of a dot.
- // If comma is not replaced with a dot, the input will be rounded down (i.e. acting
- // as a floor function) causing unexpected behaviors
- function toMs(s) {
- return Number(s.slice(0, -1).replace(",", ".")) * 1000;
- }
- function whenTransitionEnd(elm, cb) {
- const styles = window.getComputedStyle(elm);
- const delays = (styles.transitionDelay || "").split(", ");
- const durations = (styles.transitionDuration || "").split(", ");
- const timeout = getTimeout(delays, durations);
- if (timeout > 0) {
- elm.addEventListener("transitionend", cb, { once: true });
- }
- else {
- cb();
- }
- }
- QWeb.addDirective({
- name: "transition",
- priority: 96,
- atNodeCreation({ value, addNodeHook }) {
- let name = value;
- const hooks = {
- insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
- remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
- };
- for (let hookName in hooks) {
- addNodeHook(hookName, hooks[hookName]);
- }
- }
- });
- //------------------------------------------------------------------------------
- // t-widget
- //------------------------------------------------------------------------------
- /**
- * The t-widget directive is certainly a complicated and hard to maintain piece
- * of code. To help you, fellow developer, if you have to maintain it, I offer
- * you this advice: Good luck...
- *
- * Since it is not 'direct' code, but rather code that generates other code, it
- * is not easy to understand. To help you, here is a detailed and commented
- * explanation of the code generated by the t-widget directive for the following
- * situation:
- * ```xml
- *
- * ```
- *
- * ```js
- * // this is the virtual node representing the parent div
- * let c1 = [], p1 = { key: 1 };
- * var vn1 = h("div", p1, c1);
- *
- * // t-widget directive: we start by evaluating the expression given by t-key:
- * let key5 = "somestring";
- *
- * // We keep the index of the position of the widget in the closure. We push
- * // null to reserve the slot, and will replace it later by the widget vnode,
- * // when it will be ready (do not forget that preparing/rendering a widget is
- * // asynchronous)
- * let _2_index = c1.length;
- * c1.push(null);
- *
- * // def3 is the deferred that will contain later either the new widget
- * // creation, or the props update...
- * let def3;
- *
- * // this is kind of tricky: we need here to find if the widget was already
- * // created by a previous rendering. This is done by checking the internal
- * // `cmap` (children map) of the parent widget: it maps keys to widget ids,
- * // and, then, if there is an id, we look into the children list to get the
- * // instance
- * let w4 =
- * key5 in context.__owl__.cmap
- * ? context.__owl__.children[context.__owl__.cmap[key5]]
- * : false;
- *
- * // we evaluate here the props given to the component. It is done here to be
- * // able to easily reference it later, and also, it might be an expensive
- * // computation, so it is certainly better to do it only once
- * let props4 = { flag: context["state"].flag };
- *
- * // If we have a widget, currently rendering, but not ready yet, and which was
- * // rendered with different props, we do not want to wait for it to be ready,
- * // then update it. We simply destroy it, and start anew.
- * if (
- * w4 &&
- * w4.__owl__.renderPromise &&
- * !w4.__owl__.isStarted &&
- * props4 !== w4.__owl__.renderProps
- * ) {
- * w4.destroy();
- * w4 = false;
- * }
- *
- * if (!w4) {
- * // in this situation, we need to create a new widget. First step is
- * // to get a reference to the class, then create an instance with
- * // current context as parent, and the props.
- * let W4 = context.widgets["child"];
- * if (!W4) {
- * throw new Error("Cannot find the definition of widget 'child'");
- * }
- * w4 = new W4(owner, props4);
- *
- * let utils = this.utils;
- *
- * // Whenever we rerender the parent widget, we need to be sure that we
- * // are able to find the widget instance. To do that, we register it to
- * // the parent cmap (children map). Note that the 'template' key is
- * // used here, since this is what identify the widget from the template
- * // perspective.
- * context.__owl__.cmap[key5] = w4.__owl__.id;
- *
- * // _prepare is called, to basically call willStart, then render the
- * // widget
- * def3 = w4._prepare();
- *
- * def3 = def3.then(vnode => {
- * // we create here a virtual node for the parent (NOT the widget). This
- * // means that the vdom of the parent will be stopped here, and from
- * // the parent's perspective, it simply is a vnode with no children.
- * // However, it shares the same dom element with the component root
- * // vnode.
- * let pvnode = h(vnode.sel, { key: key5 });
- *
- * // we add hooks to the parent vnode so we can interact with the new
- * // widget at the proper time
- * pvnode.data.hook = {
- * insert(vn) {
- * // the _mount method will patch the widget vdom into the elm vn.elm,
- * // then call the mounted hooks. However, suprisingly, the snabbdom
- * // patch method actually replace the elm by a new elm, so we need
- * // to synchronise the pvnode elm with the resulting elm
- * let nvn = w4._mount(vnode, vn.elm);
- * pvnode.elm = nvn.elm;
- * // what follows is only present if there are animations on the widget
- * utils.transitionInsert(vn.elm, "fade");
- * },
- * remove() {
- * // override with empty function to prevent from removing the node
- * // directly. It will be removed when destroy is called anyway, which
- * // delays the removal if there are animations.
- * },
- * destroy() {
- * // if there are animations, we delay the call to destroy on the
- * // widget, if not, we call it directly.
- * let finalize = () => {
- * w4.destroy();
- * };
- * utils.transitionRemove(vn.elm, "fade", finalize);
- * }
- * };
- * // the pvnode is inserted at the correct position in the div's children
- * c1[_2_index] = pvnode;
- *
- * // we keep here a reference to the parent vnode (representing the
- * // widget, so we can reuse it later whenever we update the widget
- * w4.__owl__.pvnode = pvnode;
- * });
- * } else {
- * // this is the 'update' path of the directive.
- * // the call to _updateProps is the actual widget update
- * def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
- * def3 = def3.then(() => {
- * // if widget was destroyed in the meantime, we do nothing (so, this
- * // means that the parent's element children list will have a null in
- * // the widget's position, which will cause the pvnode to be removed
- * // when it is patched.
- * if (w4.__owl__.isDestroyed) {
- * return;
- * }
- * // like above, we register the pvnode to the children list, so it
- * // will not be patched out of the dom.
- * let pvnode = w4.__owl__.pvnode;
- * c1[_2_index] = pvnode;
- * });
- * }
- *
- * // we register the deferred here so the parent can coordinate its patch operation
- * // with all the children.
- * extra.promises.push(def3);
- * return vn1;
- * ```
- */
- QWeb.addDirective({
- name: "widget",
- extraNames: ["props", "keepalive"],
- priority: 100,
- atNodeEncounter({ ctx, value, node }) {
- ctx.addLine("//WIDGET");
- ctx.rootContext.shouldDefineOwner = true;
- ctx.rootContext.shouldDefineQWeb = true;
- ctx.rootContext.shouldDefineUtils = true;
- let props = node.getAttribute("t-props");
- let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- // t-on- events and t-transition
- const events = [];
- let transition = "";
- const attributes = node.attributes;
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- if (name.startsWith("t-on-")) {
- events.push([name.slice(5), attributes[i].textContent]);
- }
- else if (name === "t-transition") {
- transition = attributes[i].textContent;
- }
- }
- let key = node.getAttribute("t-key");
- if (key) {
- key = ctx.formatExpression(key);
- }
- if (props) {
- props = ctx.formatExpression(props);
- }
- let dummyID = ctx.generateID();
- let defID = ctx.generateID();
- let widgetID = ctx.generateID();
- let keyID = key && ctx.generateID();
- if (key) {
- // we bind a variable to the key (could be a complex expression, so we
- // want to evaluate it only once)
- ctx.addLine(`let key${keyID} = ${key};`);
- }
- ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
- ctx.addLine(`c${ctx.parentNode}.push(null);`);
- ctx.addLine(`let def${defID};`);
- let templateID = key
- ? `key${keyID}`
- : ctx.inLoop
- ? `String(-${widgetID} - i)`
- : String(widgetID);
- let ref = node.getAttribute("t-ref");
- let refExpr = "";
- let refKey = "";
- if (ref) {
- refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
- refExpr = `context.refs[${refKey}] = w${widgetID};`;
- }
- let transitionsInsertCode = "";
- if (transition) {
- transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
- }
- let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}();`;
- if (ref) {
- finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
- }
- if (transition) {
- finalizeWidgetCode = `let finalize = () => {
- ${finalizeWidgetCode}
- };
- utils.transitionRemove(vn.elm, '${transition}', finalize);`;
- }
- let createHook = "";
- let classAttr = node.getAttribute("class");
- let tattClass = node.getAttribute("t-att-class");
- let styleAttr = node.getAttribute("style");
- let tattStyle = node.getAttribute("t-att-style");
- if (tattStyle) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
- tattStyle = attVar;
- }
- let updateClassCode = "";
- if (classAttr || tattClass || styleAttr || tattStyle) {
- let classCode = "";
- if (classAttr) {
- classCode =
- classAttr
- .split(" ")
- .map(c => `vn.elm.classList.add('${c}')`)
- .join(";") + ";";
- }
- if (tattClass) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
- classCode = `for (let k in ${attVar}) {
- if (${attVar}[k]) {
- vn.elm.classList.add(k);
- }
- }`;
- updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
- }
- const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
- const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
- createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
- }
- ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`);
- ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
- ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`);
- ctx.addLine(`w${widgetID}.destroy();`);
- ctx.addLine(`w${widgetID} = false`);
- ctx.closeIf();
- ctx.addIf(`!w${widgetID}`);
- // new widget
- ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
- ctx.addLine(`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`);
- // maybe only do this in dev mode...
- ctx.addLine(`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`);
- ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
- ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`);
- for (let [event, method] of events) {
- ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
- }
- ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
- // hack: specify empty remove hook to prevent the node from being removed from the DOM
- // FIXME: click to re-add widget during remove transition -> leak
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`);
- ctx.addElse();
- // need to update widget
- ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`);
- let keepAliveCode = "";
- if (keepAlive) {
- keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
- }
- ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`);
- ctx.closeIf();
- ctx.addLine(`extra.promises.push(def${defID});`);
- if (node.hasAttribute("t-if") ||
- node.hasAttribute("t-else") ||
- node.hasAttribute("t-elif")) {
- ctx.closeIf();
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-mounted
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "mounted",
- priority: 97,
- atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
- ctx.rootContext.shouldDefineOwner = true;
- const eventName = fullName.slice(5);
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handler = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`;
- if (extraArgs) {
- ctx.addLine(`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`);
- }
- else {
- ctx.addLine(`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`);
- }
- addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
- }
- });
-
- class Store extends EventBus {
- constructor(config, options = {}) {
- super();
- this._commitLevel = 0;
- this.history = [];
- this.debug = options.debug || false;
- this.state = config.state || {};
- this.actions = config.actions;
- this.mutations = config.mutations;
- this.env = config.env;
- this.observer = new Observer();
- this.observer.notifyCB = this.trigger.bind(this, "update");
- this.observer.allowMutations = false;
- this.observer.observe(this.state);
- this.getters = {};
- if (this.debug) {
- this.history.push({ state: this.state });
- }
- this.set = this.observer.set.bind(this.observer);
- for (let entry of Object.entries(config.getters || {})) {
- const name = entry[0];
- const func = entry[1];
- this.getters[name] = payload => {
- return func({ state: this.state, getters: this.getters }, payload);
- };
- }
- }
- dispatch(action, payload) {
- if (!this.actions[action]) {
- throw new Error(`[Error] action ${action} is undefined`);
- }
- const result = this.actions[action]({
- commit: this.commit.bind(this),
- dispatch: this.dispatch.bind(this),
- env: this.env,
- state: this.state,
- getters: this.getters
- }, payload);
- if (result instanceof Promise) {
- return new Promise((resolve, reject) => {
- result.then(() => resolve());
- result.catch(reject);
- });
- }
- }
- commit(type, payload) {
- if (!this.mutations[type]) {
- throw new Error(`[Error] mutation ${type} is undefined`);
- }
- this._commitLevel++;
- this.observer.allowMutations = true;
- const res = this.mutations[type].call(null, {
- commit: this.commit.bind(this),
- state: this.state,
- set: this.set,
- getters: this.getters
- }, payload);
- if (this._commitLevel === 1) {
- this.observer.allowMutations = false;
- if (this.debug) {
- this.history.push({
- state: this.state,
- mutation: type,
- payload: payload
- });
- }
- }
- this._commitLevel--;
- return res;
- }
- }
- //------------------------------------------------------------------------------
- // Connect function
- //------------------------------------------------------------------------------
- function revNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.rev;
- }
- return 0;
- }
- function deepRevNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.deepRev;
- }
- return 0;
- }
- let nextID$1 = 1;
- function connect(mapStateToProps, options = {}) {
- let hashFunction = options.hashFunction || null;
- if (!hashFunction) {
- let deep = "deep" in options ? options.deep : true;
- let defaultRevFunction = deep ? deepRevNumber : revNumber;
- hashFunction = function ({ storeProps }, options) {
- const { currentStoreProps } = options;
- if ("__owl__" in storeProps) {
- return defaultRevFunction(storeProps);
- }
- let hash = 0;
- for (let key in storeProps) {
- const val = storeProps[key];
- const hashVal = defaultRevFunction(val);
- if (hashVal === 0) {
- if (val !== currentStoreProps[key]) {
- options.didChange = true;
- }
- }
- else {
- hash += hashVal;
- }
- }
- return hash;
- };
- }
- return function (Comp) {
- const Result = class extends Comp {
- constructor(parent, props) {
- const env = parent instanceof Component ? parent.env : parent;
- const ownProps = Object.assign({}, props || {});
- const storeProps = mapStateToProps(env.store.state, ownProps, env.store.getters);
- const mergedProps = Object.assign({}, props || {}, storeProps);
- super(parent, mergedProps);
- this.__owl__.ownProps = ownProps;
- this.__owl__.currentStoreProps = storeProps;
- this.__owl__.storeHash = hashFunction({
- state: env.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, {
- currentStoreProps: storeProps
- });
- }
- /**
- * We do not use the mounted hook here for a subtle reason: we want the
- * updates to be called for the parents before the children. However,
- * if we use the mounted hook, this will be done in the reverse order.
- */
- _callMounted() {
- this.env.store.on("update", this, this._checkUpdate);
- super._callMounted();
- }
- willUnmount() {
- this.env.store.off("update", this);
- super.willUnmount();
- }
- _checkUpdate() {
- const ownProps = this.__owl__.ownProps;
- const storeProps = mapStateToProps(this.env.store.state, ownProps, this.env.store.getters);
- const options = {
- currentStoreProps: this.__owl__.currentStoreProps
- };
- const storeHash = hashFunction({
- state: this.env.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, options);
- let didChange = options.didChange;
- if (storeHash !== this.__owl__.storeHash) {
- didChange = true;
- this.__owl__.storeHash = storeHash;
- }
- if (didChange) {
- this.__owl__.currentStoreProps = storeProps;
- this._updateProps(ownProps, false);
- }
- }
- _updateProps(nextProps, forceUpdate, patchQueue) {
- if (this.__owl__.ownProps !== nextProps) {
- this.__owl__.currentStoreProps = mapStateToProps(this.env.store.state, nextProps, this.env.store.getters);
- }
- this.__owl__.ownProps = nextProps;
- const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps);
- return super._updateProps(mergedProps, forceUpdate, patchQueue);
- }
- };
- // we assign here a unique name to the resulting anonymous class.
- // this is necessary for Owl to be able to properly deduce templates.
- // Otherwise, all connected components would have the same name, and then
- // each component after the first will necessarily have the same template.
- let name = `ConnectedComponent${nextID$1++}`;
- Object.defineProperty(Result, "name", { value: name });
- return Result;
- };
- }
-
- /**
- * Owl Utils
- *
- * We have here a small collection of utility functions:
- *
- * - whenReady
- * - loadJS
- * - loadTemplates
- * - escape
- * - debounce
- */
- function whenReady(fn) {
- if (document.readyState === "complete") {
- fn();
- }
- else {
- document.addEventListener("DOMContentLoaded", fn, false);
- }
- }
- const loadedScripts = {};
- function loadJS(url) {
- if (url in loadedScripts) {
- return loadedScripts[url];
- }
- const promise = new Promise(function (resolve, reject) {
- const script = document.createElement("script");
- script.type = "text/javascript";
- script.src = url;
- script.onload = function () {
- resolve();
- };
- script.onerror = function () {
- reject(`Error loading file '${url}'`);
- };
- const head = document.head || document.getElementsByTagName("head")[0];
- head.appendChild(script);
- });
- loadedScripts[url] = promise;
- return promise;
- }
- async function loadTemplates(url) {
- const result = await fetch(url);
- if (!result.ok) {
- throw new Error("Error while fetching xml templates");
- }
- let templates = await result.text();
- templates = templates.replace(//g, "");
- return templates;
- }
- function escape(str) {
- if (str === undefined) {
- return "";
- }
- if (typeof str === "number") {
- return String(str);
- }
- return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, "'")
- .replace(/`/g, "`");
- }
- /**
- * Returns a function, that, as long as it continues to be invoked, will not
- * be triggered. The function will be called after it stops being called for
- * N milliseconds. If `immediate` is passed, trigger the function on the
- * leading edge, instead of the trailing.
- *
- * Inspired by https://davidwalsh.name/javascript-debounce-function
- */
- function debounce(func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- function later() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- }
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
- }
-
- var _utils = /*#__PURE__*/Object.freeze({
- whenReady: whenReady,
- loadJS: loadJS,
- loadTemplates: loadTemplates,
- escape: escape,
- debounce: debounce
- });
-
- /**
- * This file is the main file packaged by rollup (see rollup.config.js). From
- * this file, we export all public owl elements.
- *
- * Note that dynamic values, such as a date or a commit hash are added by rollup
- */
- const __info__ = {};
- Object.defineProperty(__info__, "mode", {
- get() {
- return QWeb.dev ? "dev" : "prod";
- },
- set(mode) {
- QWeb.dev = mode === "dev";
- if (QWeb.dev) {
- const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
- console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
- }
- else {
- console.log(`Owl is now running in 'prod' mode.`);
- }
- }
- });
- const utils = _utils;
-
- exports.Component = Component;
- exports.EventBus = EventBus;
- exports.Observer = Observer;
- exports.QWeb = QWeb;
- exports.Store = Store;
- exports.__info__ = __info__;
- exports.connect = connect;
- exports.utils = utils;
-
- exports.__info__.version = '0.12.0';
- exports.__info__.date = '2019-05-29T11:18:30.894Z';
- exports.__info__.hash = '60cea14';
- exports.__info__.url = 'https://github.com/odoo/owl';
-
-}(this.owl = this.owl || {}));
diff --git a/tools/benchmarks/owl-0.12.0/templates.xml b/tools/benchmarks/owl-0.12.0/templates.xml
deleted file mode 100644
index 2207465d..00000000
--- a/tools/benchmarks/owl-0.12.0/templates.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/benchmarks/owl-0.14.0/index.html b/tools/benchmarks/owl-0.14.0/index.html
deleted file mode 100644
index 61cf0b1b..00000000
--- a/tools/benchmarks/owl-0.14.0/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
- OWL 0.14.0 Benchmark
-
-
-
-
-
-
-
-
diff --git a/tools/benchmarks/owl-0.14.0/owl.js b/tools/benchmarks/owl-0.14.0/owl.js
deleted file mode 100644
index c65f3baf..00000000
--- a/tools/benchmarks/owl-0.14.0/owl.js
+++ /dev/null
@@ -1,3410 +0,0 @@
-(function (exports) {
- 'use strict';
-
- /**
- * Owl Observer
- *
- * This code contains the logic that allows Owl to observe and react to state
- * changes.
- *
- * This is a Observer class that can observe any JS values. The way it works
- * can be summarized thusly:
- * - primitive values are not observed at all
- * - Objects are observed by replacing all their keys with getters/setters
- * (recursively)
- * - Arrays are observed by replacing their prototype with a customized version,
- * which wrap some methods to allow the tracking of each state change.
- *
- * Note that this code is inspired by Vue.
- */
- //------------------------------------------------------------------------------
- // Modified Array prototype
- //------------------------------------------------------------------------------
- // we define here a new modified Array prototype, which basically override all
- // Array methods that change some state to be able to track their changes
- const methodsToPatch = [
- "push",
- "pop",
- "shift",
- "unshift",
- "splice",
- "sort",
- "reverse"
- ];
- const methodLen = methodsToPatch.length;
- const ArrayProto = Array.prototype;
- const ModifiedArrayProto = Object.create(ArrayProto);
- for (let i = 0; i < methodLen; i++) {
- const method = methodsToPatch[i];
- const initialMethod = ArrayProto[method];
- ModifiedArrayProto[method] = function (...args) {
- if (!this.__observer__.allowMutations) {
- throw new Error(`Array cannot be changed here")`);
- }
- this.__observer__.rev++;
- this.__observer__.notifyChange();
- this.__owl__.rev++;
- let parent = this;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent));
- let inserted;
- switch (method) {
- case "push":
- case "unshift":
- inserted = args;
- break;
- case "splice":
- inserted = args.slice(2);
- break;
- }
- if (inserted) {
- for (let i = 0, iLen = inserted.length; i < iLen; i++) {
- this.__observer__.observe(inserted[i], this);
- }
- }
- return initialMethod.call(this, ...args);
- };
- }
- //------------------------------------------------------------------------------
- // Observer
- //------------------------------------------------------------------------------
- class Observer {
- constructor() {
- this.rev = 1;
- this.allowMutations = true;
- this.dirty = false;
- }
- static set(target, key, value) {
- if (!target.__owl__) {
- throw Error("`Observer.set()` can only be called with observed Objects or Arrays");
- }
- target.__owl__.observer.set(target, key, value);
- }
- static delete(target, key) {
- if (!target.__owl__) {
- throw Error("`Observer.delete()` can only be called with observed Objects");
- }
- target.__owl__.observer.delete(target, key);
- }
- notifyCB() { }
- notifyChange() {
- this.dirty = true;
- Promise.resolve().then(() => {
- if (this.dirty) {
- this.dirty = false;
- this.notifyCB();
- }
- });
- }
- observe(value, parent) {
- if (value === null) {
- // fun fact: typeof null === 'object'
- return;
- }
- if (typeof value !== "object") {
- return;
- }
- if ("__owl__" in value) {
- // already observed
- value.__owl__.parent = parent;
- return;
- }
- if (Array.isArray(value)) {
- this._observeArr(value, parent);
- }
- else {
- this._observeObj(value, parent);
- }
- }
- set(target, key, value) {
- let alreadyDefined = key in target &&
- Object.getOwnPropertyDescriptor(target, key).configurable === false;
- if (alreadyDefined) {
- target[key] = value;
- }
- else {
- this._addProp(target, key, value);
- this._updateRevNumber(target);
- }
- this.notifyChange();
- }
- delete(target, key) {
- delete target[key];
- this._updateRevNumber(target);
- this.notifyChange();
- }
- _observeObj(obj, parent) {
- obj.__owl__ = {
- rev: this.rev,
- deepRev: this.rev,
- parent,
- observer: this
- };
- Object.defineProperty(obj, "__owl__", { enumerable: false });
- for (let key in obj) {
- this._addProp(obj, key, obj[key]);
- }
- }
- _observeArr(arr, parent) {
- arr.__owl__ = {
- rev: this.rev,
- deepRev: this.rev,
- parent,
- observer: this
- };
- Object.defineProperty(arr, "__owl__", { enumerable: false });
- arr.__proto__ = Object.create(ModifiedArrayProto);
- arr.__proto__.__observer__ = this;
- for (let i = 0, iLen = arr.length; i < iLen; i++) {
- this.observe(arr[i], arr);
- }
- }
- _addProp(obj, key, value) {
- var self = this;
- Object.defineProperty(obj, key, {
- configurable: true,
- enumerable: true,
- get() {
- return value;
- },
- set(newVal) {
- if (newVal !== value) {
- if (!self.allowMutations) {
- throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`);
- }
- self._updateRevNumber(obj);
- value = newVal;
- self.observe(newVal, obj);
- self.notifyChange();
- }
- }
- });
- this.observe(value, obj);
- }
- _updateRevNumber(target) {
- this.rev++;
- target.__owl__.rev++;
- let parent = target;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent) && parent !== target);
- }
- }
-
- /**
- * Owl VDOM
- *
- * This file contains an implementation of a virtual DOM, which is a system that
- * can generate in-memory representations of a DOM tree, compare them, and
- * eventually change a concrete DOM tree to match its representation, in an
- * hopefully efficient way.
- *
- * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our
- * needs (see https://github.com/snabbdom/snabbdom).
- *
- * The main exported values are:
- * - interface VNode
- * - h function (a helper function to generate a vnode)
- * - patch function (to apply a vnode to an actual DOM node)
- */
- function vnode(sel, data, children, text, elm) {
- let key = data === undefined ? undefined : data.key;
- return { sel, data, children, text, elm, key };
- }
- //------------------------------------------------------------------------------
- // snabbdom.ts
- //------------------------------------------------------------------------------
- function isUndef(s) {
- return s === undefined;
- }
- function isDef(s) {
- return s !== undefined;
- }
- const emptyNode = vnode("", {}, [], undefined, undefined);
- function sameVnode(vnode1, vnode2) {
- return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
- }
- function isVnode(vnode) {
- return vnode.sel !== undefined;
- }
- function createKeyToOldIdx(children, beginIdx, endIdx) {
- let i, map = {}, key, ch;
- for (i = beginIdx; i <= endIdx; ++i) {
- ch = children[i];
- if (ch != null) {
- key = ch.key;
- if (key !== undefined)
- map[key] = i;
- }
- }
- return map;
- }
- const hooks = [
- "create",
- "update",
- "remove",
- "destroy",
- "pre",
- "post"
- ];
- function init(modules, domApi) {
- let i, j, cbs = {};
- const api = domApi !== undefined ? domApi : htmlDomApi;
- for (i = 0; i < hooks.length; ++i) {
- cbs[hooks[i]] = [];
- for (j = 0; j < modules.length; ++j) {
- const hook = modules[j][hooks[i]];
- if (hook !== undefined) {
- cbs[hooks[i]].push(hook);
- }
- }
- }
- function emptyNodeAt(elm) {
- const id = elm.id ? "#" + elm.id : "";
- const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
- return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
- }
- function createRmCb(childElm, listeners) {
- return function rmCb() {
- if (--listeners === 0) {
- const parent = api.parentNode(childElm);
- api.removeChild(parent, childElm);
- }
- };
- }
- function createElm(vnode, insertedVnodeQueue) {
- let i, iLen, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.init))) {
- i(vnode);
- data = vnode.data;
- }
- }
- let children = vnode.children, sel = vnode.sel;
- if (sel === "!") {
- if (isUndef(vnode.text)) {
- vnode.text = "";
- }
- vnode.elm = api.createComment(vnode.text);
- }
- else if (sel !== undefined) {
- // Parse selector
- const hashIdx = sel.indexOf("#");
- const dotIdx = sel.indexOf(".", hashIdx);
- const hash = hashIdx > 0 ? hashIdx : sel.length;
- const dot = dotIdx > 0 ? dotIdx : sel.length;
- const tag = hashIdx !== -1 || dotIdx !== -1
- ? sel.slice(0, Math.min(hash, dot))
- : sel;
- const elm = (vnode.elm =
- isDef(data) && isDef((i = data.ns))
- ? api.createElementNS(i, tag)
- : api.createElement(tag));
- if (hash < dot)
- elm.setAttribute("id", sel.slice(hash + 1, dot));
- if (dotIdx > 0)
- elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
- for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
- cbs.create[i](emptyNode, vnode);
- if (array(children)) {
- for (i = 0, iLen = children.length; i < iLen; ++i) {
- const ch = children[i];
- if (ch != null) {
- api.appendChild(elm, createElm(ch, insertedVnodeQueue));
- }
- }
- }
- else if (primitive(vnode.text)) {
- api.appendChild(elm, api.createTextNode(vnode.text));
- }
- i = vnode.data.hook; // Reuse variable
- if (isDef(i)) {
- if (i.create)
- i.create(emptyNode, vnode);
- if (i.insert)
- insertedVnodeQueue.push(vnode);
- }
- }
- else {
- vnode.elm = api.createTextNode(vnode.text);
- }
- return vnode.elm;
- }
- function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
- for (; startIdx <= endIdx; ++startIdx) {
- const ch = vnodes[startIdx];
- if (ch != null) {
- api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
- }
- }
- }
- function invokeDestroyHook(vnode) {
- let i, iLen, j, jLen, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.destroy)))
- i(vnode);
- for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
- cbs.destroy[i](vnode);
- if (vnode.children !== undefined) {
- for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
- i = vnode.children[j];
- if (i != null && typeof i !== "string") {
- invokeDestroyHook(i);
- }
- }
- }
- }
- }
- function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
- for (; startIdx <= endIdx; ++startIdx) {
- let i, iLen, listeners, rm, ch = vnodes[startIdx];
- if (ch != null) {
- if (isDef(ch.sel)) {
- invokeDestroyHook(ch);
- listeners = cbs.remove.length + 1;
- rm = createRmCb(ch.elm, listeners);
- for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
- cbs.remove[i](ch, rm);
- if (isDef((i = ch.data)) &&
- isDef((i = i.hook)) &&
- isDef((i = i.remove))) {
- i(ch, rm);
- }
- else {
- rm();
- }
- }
- else {
- // Text node
- api.removeChild(parentElm, ch.elm);
- }
- }
- }
- }
- function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
- let oldStartIdx = 0, newStartIdx = 0;
- let oldEndIdx = oldCh.length - 1;
- let oldStartVnode = oldCh[0];
- let oldEndVnode = oldCh[oldEndIdx];
- let newEndIdx = newCh.length - 1;
- let newStartVnode = newCh[0];
- let newEndVnode = newCh[newEndIdx];
- let oldKeyToIdx;
- let idxInOld;
- let elmToMove;
- let before;
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
- if (oldStartVnode == null) {
- oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
- }
- else if (oldEndVnode == null) {
- oldEndVnode = oldCh[--oldEndIdx];
- }
- else if (newStartVnode == null) {
- newStartVnode = newCh[++newStartIdx];
- }
- else if (newEndVnode == null) {
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newStartVnode)) {
- patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
- oldStartVnode = oldCh[++oldStartIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else if (sameVnode(oldEndVnode, newEndVnode)) {
- patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
- oldEndVnode = oldCh[--oldEndIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newEndVnode)) {
- // Vnode moved right
- patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
- oldStartVnode = oldCh[++oldStartIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldEndVnode, newStartVnode)) {
- // Vnode moved left
- patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode = oldCh[--oldEndIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- if (oldKeyToIdx === undefined) {
- oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
- }
- idxInOld = oldKeyToIdx[newStartVnode.key];
- if (isUndef(idxInOld)) {
- // New element
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- elmToMove = oldCh[idxInOld];
- if (elmToMove.sel !== newStartVnode.sel) {
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- }
- else {
- patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
- oldCh[idxInOld] = undefined;
- api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
- }
- newStartVnode = newCh[++newStartIdx];
- }
- }
- }
- if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
- if (oldStartIdx > oldEndIdx) {
- before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
- addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
- }
- else {
- removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
- }
- }
- }
- function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
- let i, iLen, hook;
- if (isDef((i = vnode.data)) &&
- isDef((hook = i.hook)) &&
- isDef((i = hook.prepatch))) {
- i(oldVnode, vnode);
- }
- const elm = (vnode.elm = oldVnode.elm);
- let oldCh = oldVnode.children;
- let ch = vnode.children;
- if (oldVnode === vnode)
- return;
- if (vnode.data !== undefined) {
- for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
- cbs.update[i](oldVnode, vnode);
- i = vnode.data.hook;
- if (isDef(i) && isDef((i = i.update)))
- i(oldVnode, vnode);
- }
- if (isUndef(vnode.text)) {
- if (isDef(oldCh) && isDef(ch)) {
- if (oldCh !== ch)
- updateChildren(elm, oldCh, ch, insertedVnodeQueue);
- }
- else if (isDef(ch)) {
- if (isDef(oldVnode.text))
- api.setTextContent(elm, "");
- addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
- }
- else if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- else if (isDef(oldVnode.text)) {
- api.setTextContent(elm, "");
- }
- }
- else if (oldVnode.text !== vnode.text) {
- if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- api.setTextContent(elm, vnode.text);
- }
- if (isDef(hook) && isDef((i = hook.postpatch))) {
- i(oldVnode, vnode);
- }
- }
- return function patch(oldVnode, vnode) {
- let i, iLen, elm, parent;
- const insertedVnodeQueue = [];
- for (i = 0, iLen = cbs.pre.length; i < iLen; ++i)
- cbs.pre[i]();
- if (!isVnode(oldVnode)) {
- oldVnode = emptyNodeAt(oldVnode);
- }
- if (sameVnode(oldVnode, vnode)) {
- patchVnode(oldVnode, vnode, insertedVnodeQueue);
- }
- else {
- elm = oldVnode.elm;
- parent = api.parentNode(elm);
- createElm(vnode, insertedVnodeQueue);
- if (parent !== null) {
- api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
- removeVnodes(parent, [oldVnode], 0, 0);
- }
- }
- for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
- insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
- }
- for (i = 0, iLen = cbs.post.length; i < iLen; ++i)
- cbs.post[i]();
- return vnode;
- };
- }
- //------------------------------------------------------------------------------
- // is.ts
- //------------------------------------------------------------------------------
- const array = Array.isArray;
- function primitive(s) {
- return typeof s === "string" || typeof s === "number";
- }
- function createElement(tagName) {
- return document.createElement(tagName);
- }
- function createElementNS(namespaceURI, qualifiedName) {
- return document.createElementNS(namespaceURI, qualifiedName);
- }
- function createTextNode(text) {
- return document.createTextNode(text);
- }
- function createComment(text) {
- return document.createComment(text);
- }
- function insertBefore(parentNode, newNode, referenceNode) {
- parentNode.insertBefore(newNode, referenceNode);
- }
- function removeChild(node, child) {
- node.removeChild(child);
- }
- function appendChild(node, child) {
- node.appendChild(child);
- }
- function parentNode(node) {
- return node.parentNode;
- }
- function nextSibling(node) {
- return node.nextSibling;
- }
- function tagName(elm) {
- return elm.tagName;
- }
- function setTextContent(node, text) {
- node.textContent = text;
- }
- function getTextContent(node) {
- return node.textContent;
- }
- function isElement(node) {
- return node.nodeType === 1;
- }
- function isText(node) {
- return node.nodeType === 3;
- }
- function isComment(node) {
- return node.nodeType === 8;
- }
- const htmlDomApi = {
- createElement,
- createElementNS,
- createTextNode,
- createComment,
- insertBefore,
- removeChild,
- appendChild,
- parentNode,
- nextSibling,
- tagName,
- setTextContent,
- getTextContent,
- isElement,
- isText,
- isComment
- };
- function addNS(data, children, sel) {
- data.ns = "http://www.w3.org/2000/svg";
- if (sel !== "foreignObject" && children !== undefined) {
- for (let i = 0, iLen = children.length; i < iLen; ++i) {
- let childData = children[i].data;
- if (childData !== undefined) {
- addNS(childData, children[i].children, children[i].sel);
- }
- }
- }
- }
- function h(sel, b, c) {
- var data = {}, children, text, i, iLen;
- if (c !== undefined) {
- data = b;
- if (array(c)) {
- children = c;
- }
- else if (primitive(c)) {
- text = c;
- }
- else if (c && c.sel) {
- children = [c];
- }
- }
- else if (b !== undefined) {
- if (array(b)) {
- children = b;
- }
- else if (primitive(b)) {
- text = b;
- }
- else if (b && b.sel) {
- children = [b];
- }
- else {
- data = b;
- }
- }
- if (children !== undefined) {
- for (i = 0, iLen = children.length; i < iLen; ++i) {
- if (primitive(children[i]))
- children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
- }
- }
- if (sel[0] === "s" &&
- sel[1] === "v" &&
- sel[2] === "g" &&
- (sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
- addNS(data, children, sel);
- }
- return vnode(sel, data, children, text, undefined);
- }
- function updateProps(oldVnode, vnode) {
- var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
- if (!oldProps && !props)
- return;
- if (oldProps === props)
- return;
- oldProps = oldProps || {};
- props = props || {};
- for (key in oldProps) {
- if (!props[key]) {
- delete elm[key];
- }
- }
- for (key in props) {
- cur = props[key];
- old = oldProps[key];
- if (old !== cur && (key !== "value" || elm[key] !== cur)) {
- elm[key] = cur;
- }
- }
- }
- const propsModule = {
- create: updateProps,
- update: updateProps
- };
- function invokeHandler(handler, vnode, event) {
- if (typeof handler === "function") {
- // call function handler
- handler.call(vnode, event, vnode);
- }
- else if (typeof handler === "object") {
- // call handler with arguments
- if (typeof handler[0] === "function") {
- // special case for single argument for performance
- if (handler.length === 2) {
- handler[0].call(vnode, handler[1], event, vnode);
- }
- else {
- var args = handler.slice(1);
- args.push(event);
- args.push(vnode);
- handler[0].apply(vnode, args);
- }
- }
- else {
- // call multiple handlers
- for (let i = 0, iLen = handler.length; i < iLen; i++) {
- invokeHandler(handler[i], vnode, event);
- }
- }
- }
- }
- function handleEvent(event, vnode) {
- var name = event.type, on = vnode.data.on;
- // call event handler(s) if exists
- if (on && on[name]) {
- invokeHandler(on[name], vnode, event);
- }
- }
- function createListener() {
- return function handler(event) {
- handleEvent(event, handler.vnode);
- };
- }
- function updateEventListeners(oldVnode, vnode) {
- var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name;
- // optimization for reused immutable handlers
- if (oldOn === on) {
- return;
- }
- // remove existing listeners which no longer used
- if (oldOn && oldListener) {
- // if element changed or deleted we remove all existing listeners unconditionally
- if (!on) {
- for (name in oldOn) {
- // remove listener if element was changed or existing listeners removed
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- else {
- for (name in oldOn) {
- // remove listener if existing listener removed
- if (!on[name]) {
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- }
- }
- // add new listeners which has not already attached
- if (on) {
- // reuse existing listener or create new
- var listener = (vnode.listener =
- oldVnode.listener || createListener());
- // update vnode for listener
- listener.vnode = vnode;
- // if element changed or added we add all needed listeners unconditionally
- if (!oldOn) {
- for (name in on) {
- // add listener if element was changed or new listeners added
- elm.addEventListener(name, listener, false);
- }
- }
- else {
- for (name in on) {
- // add listener if new listener added
- if (!oldOn[name]) {
- elm.addEventListener(name, listener, false);
- }
- }
- }
- }
- }
- const eventListenersModule = {
- create: updateEventListeners,
- update: updateEventListeners,
- destroy: updateEventListeners
- };
- const xlinkNS = "http://www.w3.org/1999/xlink";
- const xmlNS = "http://www.w3.org/XML/1998/namespace";
- const colonChar = 58;
- const xChar = 120;
- function updateAttrs(oldVnode, vnode) {
- var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs;
- if (!oldAttrs && !attrs)
- return;
- if (oldAttrs === attrs)
- return;
- oldAttrs = oldAttrs || {};
- attrs = attrs || {};
- // update modified attributes, add new attributes
- for (key in attrs) {
- const cur = attrs[key];
- const old = oldAttrs[key];
- if (old !== cur) {
- if (cur === true) {
- elm.setAttribute(key, "");
- }
- else if (cur === false) {
- elm.removeAttribute(key);
- }
- else {
- if (key.charCodeAt(0) !== xChar) {
- elm.setAttribute(key, cur);
- }
- else if (key.charCodeAt(3) === colonChar) {
- // Assume xml namespace
- elm.setAttributeNS(xmlNS, key, cur);
- }
- else if (key.charCodeAt(5) === colonChar) {
- // Assume xlink namespace
- elm.setAttributeNS(xlinkNS, key, cur);
- }
- else {
- elm.setAttribute(key, cur);
- }
- }
- }
- }
- // remove removed attributes
- // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
- // the other option is to remove all attributes with value == undefined
- for (key in oldAttrs) {
- if (!(key in attrs)) {
- elm.removeAttribute(key);
- }
- }
- }
- const attrsModule = {
- create: updateAttrs,
- update: updateAttrs
- };
- const patch = init([eventListenersModule, attrsModule, propsModule]);
-
- /**
- * Owl QWeb Expression Parser
- *
- * Owl needs in various contexts to be able to understand the structure of a
- * string representing a javascript expression. The usual goal is to be able
- * to rewrite some variables. For example, if a template has
- *
- * ```xml
- * ...
- * ```
- *
- * this needs to be translated in something like this:
- *
- * ```js
- * if (context["computeSomething"]({val: context["state"].val})) { ... }
- * ```
- *
- * This file contains the implementation of an extremely naive tokenizer/parser
- * and evaluator for javascript expressions. The supported grammar is basically
- * only expressive enough to understand the shape of objects, of arrays, and
- * various operators.
- */
- //------------------------------------------------------------------------------
- // Misc types, constants and helpers
- //------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const STATIC_TOKEN_MAP = {
- "{": "LEFT_BRACE",
- "}": "RIGHT_BRACE",
- "[": "LEFT_BRACKET",
- "]": "RIGHT_BRACKET",
- ":": "COLON",
- ",": "COMMA",
- "(": "LEFT_PAREN",
- ")": "RIGHT_PAREN"
- };
- const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(',');
- let tokenizeString = function (expr) {
- let s = expr[0];
- let start = s;
- if (s !== "'" && s !== '"') {
- return false;
- }
- let i = 1;
- let cur;
- while (expr[i] && expr[i] !== start) {
- cur = expr[i];
- s += cur;
- if (cur === "\\") {
- i++;
- cur = expr[i];
- if (!cur) {
- throw new Error("Invalid expression");
- }
- s += cur;
- }
- i++;
- }
- if (expr[i] !== start) {
- throw new Error("Invalid expression");
- }
- s += start;
- return { type: "VALUE", value: s };
- };
- let tokenizeNumber = function (expr) {
- let s = expr[0];
- if (s && s.match(/[0-9]/)) {
- let i = 1;
- while (expr[i] && expr[i].match(/[0-9]|\./)) {
- s += expr[i];
- i++;
- }
- return { type: "VALUE", value: s };
- }
- else {
- return false;
- }
- };
- let tokenizeSymbol = function (expr) {
- let s = expr[0];
- if (s && s.match(/[a-zA-Z_\$]/)) {
- let i = 1;
- while (expr[i] && expr[i].match(/\w/)) {
- s += expr[i];
- i++;
- }
- if (s in WORD_REPLACEMENT) {
- return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
- }
- return { type: "SYMBOL", value: s };
- }
- else {
- return false;
- }
- };
- const tokenizeStatic = function (expr) {
- const char = expr[0];
- if (char && char in STATIC_TOKEN_MAP) {
- return { type: STATIC_TOKEN_MAP[char], value: char };
- }
- return false;
- };
- const tokenizeOperator = function (expr) {
- for (let op of OPERATORS) {
- if (expr.startsWith(op)) {
- return { type: "OPERATOR", value: op };
- }
- }
- return false;
- };
- const TOKENIZERS = [
- tokenizeString,
- tokenizeNumber,
- tokenizeSymbol,
- tokenizeStatic,
- tokenizeOperator
- ];
- /**
- * Convert a javascript expression (as a string) into a list of tokens. For
- * example: `tokenize("1 + b")` will return:
- * ```js
- * [
- * {type: "VALUE", value: "1"},
- * {type: "OPERATOR", value: "+"},
- * {type: "SYMBOL", value: "b"}
- * ]
- * ```
- */
- function tokenize(expr) {
- const result = [];
- let token = true;
- while (token) {
- expr = expr.trim();
- if (expr) {
- for (let tokenizer of TOKENIZERS) {
- token = tokenizer(expr);
- if (token) {
- result.push(token);
- expr = expr.slice(token.size || token.value.length);
- break;
- }
- }
- }
- else {
- token = false;
- }
- }
- if (expr.length) {
- throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
- }
- return result;
- }
- //------------------------------------------------------------------------------
- // Expression "evaluator"
- //------------------------------------------------------------------------------
- /**
- * This is the main function exported by this file. This is the code that will
- * process an expression (given as a string) and returns another expression with
- * proper lookups in the context.
- *
- * Usually, this kind of code would be very simple to do if we had an AST (so,
- * if we had a javascript parser), since then, we would only need to find the
- * variables and replace them. However, a parser is more complicated, and there
- * are no standard builtin parser API.
- *
- * Since this method is applied to simple javasript expressions, and the work to
- * be done is actually quite simple, we actually can get away with not using a
- * parser, which helps with the code size.
- *
- * Here is the heuristic used by this method to determine if a token is a
- * variable:
- * - by default, all symbols are considered a variable
- * - unless the previous token is a dot (in that case, this is a property: `a.b`)
- * - or if the previous token is a left brace or a comma, and the next token is
- * a colon (in that case, this is an object key: `{a: b}`)
- */
- function compileExpr(expr, vars) {
- const tokens = tokenize(expr);
- let result = "";
- for (let i = 0; i < tokens.length; i++) {
- let token = tokens[i];
- if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
- // we need to find if it is a variable
- let isVar = true;
- let prevToken = tokens[i - 1];
- if (prevToken) {
- if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
- isVar = false;
- }
- else if (prevToken.type === "LEFT_BRACE" ||
- prevToken.type === "COMMA") {
- let nextToken = tokens[i + 1];
- if (nextToken && nextToken.type === "COLON") {
- isVar = false;
- }
- }
- }
- if (isVar) {
- if (token.value in vars && "id" in vars[token.value]) {
- token.value = vars[token.value].id;
- }
- else {
- token.value = `context['${token.value}']`;
- }
- }
- }
- result += token.value;
- }
- return result;
- }
-
- //------------------------------------------------------------------------------
- // Const/global stuff/helpers
- //------------------------------------------------------------------------------
- const DISABLED_TAGS = [
- "input",
- "textarea",
- "button",
- "select",
- "option",
- "optgroup"
- ];
- const lineBreakRE = /[\r\n]/;
- const whitespaceRE = /\s+/g;
- const DIRECTIVE_NAMES = {
- name: 1,
- att: 1,
- attf: 1,
- key: 1
- };
- const DIRECTIVES = [];
- const NODE_HOOKS_PARAMS = {
- create: "(_, n)",
- insert: "vn",
- remove: "(vn, rm)"
- };
- const UTILS = {
- h: h,
- objectToAttrString(obj) {
- let classes = [];
- for (let k in obj) {
- if (obj[k]) {
- classes.push(k);
- }
- }
- return classes.join(" ");
- },
- shallowEqual(p1, p2) {
- for (let k in p1) {
- if (p1[k] !== p2[k]) {
- return false;
- }
- }
- return true;
- }
- };
- function parseXML(xml) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
- }
- return doc;
- }
- let nextID = 1;
- //------------------------------------------------------------------------------
- // QWeb rendering engine
- //------------------------------------------------------------------------------
- class QWeb {
- constructor(data) {
- this.templates = {};
- this.utils = UTILS;
- // the id field is useful to be able to hash qweb instances. The current
- // use case is that component's templates are qweb dependant, and need to be
- // able to map a qweb instance to a template name.
- this.id = nextID++;
- // slots contains sub templates defined with t-set inside t-widget nodes, and
- // are meant to be used by the t-slot directive.
- this.slots = {};
- this.nextSlotId = 1;
- if (data) {
- this.addTemplates(data);
- }
- this.addTemplate("default", "");
- }
- static addDirective(directive) {
- DIRECTIVES.push(directive);
- DIRECTIVE_NAMES[directive.name] = 1;
- DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
- if (directive.extraNames) {
- directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
- }
- }
- static register(name, Component) {
- if (QWeb.widgets[name]) {
- throw new Error(`Component '${name}' has already been registered`);
- }
- QWeb.widgets[name] = Component;
- }
- /**
- * Add a template to the internal template map. Note that it is not
- * immediately compiled.
- */
- addTemplate(name, xmlString) {
- const doc = parseXML(xmlString);
- if (!doc.firstChild) {
- throw new Error("Invalid template (should not be empty)");
- }
- this._addTemplate(name, doc.firstChild);
- }
- /**
- * Load templates from a xml (as a string). This will look up for the first
- * tag, and will consider each child of this as a template, with
- * the name given by the t-name attribute.
- */
- addTemplates(xmlstr) {
- const doc = parseXML(xmlstr);
- const templates = doc.getElementsByTagName("templates")[0];
- if (!templates) {
- return;
- }
- for (let elem of templates.children) {
- const name = elem.getAttribute("t-name");
- this._addTemplate(name, elem);
- }
- }
- _addTemplate(name, elem) {
- if (name in this.templates) {
- throw new Error(`Template ${name} already defined`);
- }
- this._processTemplate(elem);
- const template = {
- elem,
- fn: (context, extra) => {
- const compiledFunction = this._compile(name, elem);
- template.fn = compiledFunction;
- return compiledFunction.call(this, context, extra);
- }
- };
- this.templates[name] = template;
- }
- _processTemplate(elem) {
- let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
- let node = tbranch[i];
- let prevElem = node.previousElementSibling;
- let pattr = function (name) {
- return prevElem.getAttribute(name);
- };
- let nattr = function (name) {
- return +!!node.getAttribute(name);
- };
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
- if (pattr("t-foreach")) {
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
- }
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
- return a + b;
- }) > 1) {
- throw new Error("Only one conditional branching directive is allowed per node");
- }
- // All text nodes between branch nodes are removed
- let textNode;
- while ((textNode = node.previousSibling) !== prevElem) {
- if (textNode.nodeValue.trim().length) {
- throw new Error("text is not allowed between branching directives");
- }
- textNode.remove();
- }
- }
- else {
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
- }
- }
- }
- /**
- * Render a template
- *
- * @param {string} name the template should already have been added
- */
- render(name, context = {}, extra = null) {
- const template = this.templates[name];
- if (!template) {
- throw new Error(`Template ${name} does not exist`);
- }
- return template.fn.call(this, context, extra);
- }
- _compile(name, elem) {
- const isDebug = elem.attributes.hasOwnProperty("t-debug");
- const ctx = new Context(name);
- this._compileNode(elem, ctx);
- if (ctx.shouldProtectContext) {
- ctx.code.unshift(" context = Object.create(context);");
- }
- if (ctx.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- ctx.code.unshift(" let owner = context;");
- }
- if (ctx.shouldDefineQWeb) {
- ctx.code.unshift(" let QWeb = this.constructor;");
- }
- if (ctx.shouldDefineUtils) {
- ctx.code.unshift(" let utils = this.utils;");
- }
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
- }
- ctx.addLine(`return vn${ctx.rootNode};`);
- let template;
- try {
- template = new Function("context", "extra", ctx.code.join("\n"));
- }
- catch (e) {
- const templateName = ctx.templateName.replace(/`/g, "'");
- console.groupCollapsed(`Invalid Code generated by ${templateName}`);
- console.warn(ctx.code.join("\n"));
- console.groupEnd();
- throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`);
- }
- if (isDebug) {
- console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
- template.toString());
- }
- return template;
- }
- /**
- * Generate code from an xml node
- *
- */
- _compileNode(node, ctx) {
- if (!(node instanceof Element)) {
- // this is a text node, there are no directive to apply
- let text = node.textContent;
- if (!ctx.inPreTag) {
- if (lineBreakRE.test(text) && !text.trim()) {
- return;
- }
- text = text.replace(whitespaceRE, " ");
- }
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
- }
- else if (ctx.parentTextNode) {
- ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
- }
- else {
- // this is an unusual situation: this text node is the result of the
- // template rendering.
- let nodeID = ctx.generateID();
- ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
- ctx.rootContext.rootNode = nodeID;
- ctx.rootContext.parentTextNode = nodeID;
- }
- return;
- }
- const attributes = node.attributes;
- const validDirectives = [];
- let withHandlers = false;
- // maybe this is not optimal: we iterate on all attributes here, and again
- // just after for each directive.
- for (let i = 0; i < attributes.length; i++) {
- let attrName = attributes[i].name;
- if (attrName.startsWith("t-")) {
- let dName = attrName.slice(2).split("-")[0];
- if (!(dName in DIRECTIVE_NAMES)) {
- throw new Error(`Unknown QWeb directive: '${attrName}'`);
- }
- }
- }
- const DIR_N = DIRECTIVES.length;
- const ATTR_N = attributes.length;
- for (let i = 0; i < DIR_N; i++) {
- let directive = DIRECTIVES[i];
- let fullName;
- let value;
- for (let j = 0; j < ATTR_N; j++) {
- const name = attributes[j].name;
- if (name === "t-" + directive.name ||
- name.startsWith("t-" + directive.name + "-")) {
- fullName = name;
- value = attributes[j].textContent;
- validDirectives.push({ directive, value, fullName });
- if (directive.name === "on") {
- withHandlers = true;
- }
- }
- }
- }
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeEncounter) {
- const isDone = directive.atNodeEncounter({
- node,
- qweb: this,
- ctx,
- fullName,
- value
- });
- if (isDone) {
- return;
- }
- }
- }
- if (node.nodeName !== "t") {
- let nodeID = this._compileGenericNode(node, ctx, withHandlers);
- ctx = ctx.withParent(nodeID);
- let nodeHooks = {};
- let addNodeHook = function (hook, handler) {
- nodeHooks[hook] = nodeHooks[hook] || [];
- nodeHooks[hook].push(handler);
- };
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeCreation) {
- directive.atNodeCreation({
- node,
- qweb: this,
- ctx,
- fullName,
- value,
- nodeID,
- addNodeHook
- });
- }
- }
- if (Object.keys(nodeHooks).length) {
- ctx.addLine(`p${nodeID}.hook = {`);
- for (let hook in nodeHooks) {
- ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
- for (let handler of nodeHooks[hook]) {
- ctx.addLine(` ${handler}`);
- }
- ctx.addLine(` },`);
- }
- ctx.addLine(`};`);
- }
- }
- if (node.nodeName === "pre") {
- ctx = ctx.subContext("inPreTag", true);
- }
- this._compileChildren(node, ctx);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.finalize) {
- directive.finalize({ node, qweb: this, ctx, fullName, value });
- }
- }
- }
- _compileGenericNode(node, ctx, withHandlers = true) {
- // nodeType 1 is generic tag
- if (node.nodeType !== 1) {
- throw new Error("unsupported node type");
- }
- const attributes = node.attributes;
- const attrs = [];
- const props = [];
- const tattrs = [];
- function handleBooleanProps(key, val) {
- let isProp = false;
- if (node.nodeName === "input" && key === "checked") {
- let type = node.getAttribute("type");
- if (type === "checkbox" || type === "radio") {
- isProp = true;
- }
- }
- if (node.nodeName === "option" && key === "selected") {
- isProp = true;
- }
- if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
- isProp = true;
- }
- if ((key === "readonly" && node.nodeName === "input") ||
- node.nodeName === "textarea") {
- isProp = true;
- }
- if (isProp) {
- props.push(`${key}: _${val}`);
- }
- }
- for (let i = 0; i < attributes.length; i++) {
- let name = attributes[i].name;
- const value = attributes[i].textContent;
- // regular attributes
- if (!name.startsWith("t-") &&
- !node.getAttribute("t-attf-" + name)) {
- const attID = ctx.generateID();
- ctx.addLine(`var _${attID} = '${value}';`);
- if (!name.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- name = '"' + name + '"';
- }
- attrs.push(`${name}: _${attID}`);
- handleBooleanProps(name, attID);
- }
- // dynamic attributes
- if (name.startsWith("t-att-")) {
- let attName = name.slice(6);
- const v = ctx.getValue(value);
- let formattedValue = v.id || ctx.formatExpression(v);
- if (formattedValue[0] === "{" &&
- formattedValue[formattedValue.length - 1] === "}") {
- formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
- }
- const attID = ctx.generateID();
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- // we need to combine dynamic with non dynamic attributes:
- // class="a" t-att-class="'yop'" should be rendered as class="a yop"
- const attValue = node.getAttribute(attName);
- if (attValue) {
- const attValueID = ctx.generateID();
- ctx.addLine(`var _${attValueID} = ${formattedValue};`);
- formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
- const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
- attrs.splice(attrIndex, 1);
- }
- ctx.addLine(`var _${attID} = ${formattedValue};`);
- attrs.push(`${attName}: _${attID}`);
- handleBooleanProps(attName, attID);
- }
- if (name.startsWith("t-attf-")) {
- let attName = name.slice(7);
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- const formattedExpr = ctx.interpolate(value);
- const attID = ctx.generateID();
- let staticVal = node.getAttribute(attName);
- if (staticVal) {
- ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
- }
- else {
- ctx.addLine(`var _${attID} = ${formattedExpr};`);
- }
- attrs.push(`${attName}: _${attID}`);
- }
- // t-att= attributes
- if (name === "t-att") {
- let id = ctx.generateID();
- ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`);
- tattrs.push(id);
- }
- }
- let nodeID = ctx.generateID();
- let nodeKey = node.getAttribute("t-key");
- if (nodeKey) {
- nodeKey = ctx.formatExpression(nodeKey);
- }
- else {
- nodeKey = nodeID;
- }
- const parts = [`key:${nodeKey}`];
- if (attrs.length + tattrs.length > 0) {
- parts.push(`attrs:{${attrs.join(",")}}`);
- }
- if (props.length > 0) {
- parts.push(`props:{${props.join(",")}}`);
- }
- if (withHandlers) {
- parts.push(`on:{}`);
- }
- ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
- for (let id of tattrs) {
- ctx.addIf(`_${id} instanceof Array`);
- ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
- ctx.addElse();
- ctx.addLine(`for (let key in _${id}) {`);
- ctx.indent();
- ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
- ctx.dedent();
- ctx.addLine(`}`);
- ctx.closeIf();
- }
- ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
- }
- return nodeID;
- }
- _compileChildren(node, ctx) {
- if (node.childNodes.length > 0) {
- for (let child of Array.from(node.childNodes)) {
- this._compileNode(child, ctx);
- }
- }
- }
- }
- QWeb.widgets = Object.create(null);
- // dev mode enables better error messages or more costly validations
- QWeb.dev = false;
- //------------------------------------------------------------------------------
- // Compilation Context
- //------------------------------------------------------------------------------
- class Context {
- constructor(name) {
- this.nextID = 1;
- this.code = [];
- this.variables = {};
- this.escaping = false;
- this.parentNode = null;
- this.parentTextNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldDefineQWeb = false;
- this.shouldDefineUtils = false;
- this.shouldProtectContext = false;
- this.inLoop = false;
- this.inPreTag = false;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.addLine("var h = this.utils.h;");
- }
- generateID() {
- const id = this.rootContext.nextID++;
- return id;
- }
- withParent(node) {
- if (this === this.rootContext && (this.parentNode || this.parentTextNode)) {
- throw new Error("A template should not have more than one root node");
- }
- if (!this.rootContext.rootNode) {
- this.rootContext.rootNode = node;
- }
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- /**
- * Prepare an expression for being consumed at render time. Its main job
- * is to
- * - replace unknown variables by a lookup in the context
- * - replace already defined variables by their internal name
- */
- formatExpression(expr) {
- return compileExpr(expr, this.variables);
- }
- /**
- * Perform string interpolation on the given string. Note that if the whole
- * string is an expression, it simply returns it (formatted and enclosed in
- * parentheses).
- * For instance:
- * 'Hello {{x}}!' -> `Hello ${x}`
- * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
- */
- interpolate(s) {
- let matches = s.match(/\{\{.*?\}\}/g);
- if (matches && matches[0].length === s.length) {
- return `(${this.formatExpression(s.slice(2, -2))})`;
- }
- let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
- return "`" + r + "`";
- }
- }
-
- // If a component does not define explicitely a template
- // key, it needs to find a template with its name (or a parent's). This is
- // qweb dependant, so we need a place to store this information indexed by
- // qweb instances.
- const TEMPLATE_MAP = {};
- //------------------------------------------------------------------------------
- // Widget
- //------------------------------------------------------------------------------
- let nextId = 1;
- class Component {
- //--------------------------------------------------------------------------
- // Lifecycle
- //--------------------------------------------------------------------------
- /**
- * Creates an instance of Component.
- *
- * The root widget of a component tree needs an environment:
- *
- * ```javascript
- * const root = new RootWidget(env, props);
- * ```
- *
- * Every other widget simply needs a reference to its parent:
- *
- * ```javascript
- * const child = new SomeWidget(parent, props);
- * ```
- *
- * Note that most of the time, only the root widget needs to be created by
- * hand. Other widgets should be created automatically by the framework (with
- * the t-widget directive in a template)
- */
- constructor(parent, props) {
- this.refs = {};
- const defaultProps = this.constructor.defaultProps;
- if (defaultProps) {
- props = this._applyDefaultProps(props, defaultProps);
- }
- if (QWeb.dev) {
- this._validateProps(props || {});
- }
- // is this a good idea?
- // Pro: if props is empty, we can create easily a widget
- // Con: this is not really safe
- // Pro: but creating widget (by a template) is always unsafe anyway
- this.props = props || {};
- let id = nextId++;
- let p = null;
- if (parent instanceof Component) {
- p = parent;
- this.env = parent.env;
- parent.__owl__.children[id] = this;
- }
- else {
- this.env = parent;
- }
- this.__owl__ = {
- id: id,
- vnode: null,
- isMounted: false,
- isDestroyed: false,
- parent: p,
- children: {},
- cmap: {},
- renderId: 1,
- renderPromise: null,
- renderProps: props || null,
- boundHandlers: {},
- mountedHandlers: {}
- };
- }
- /**
- * The `el` is the root element of the widget. Note that it could be null:
- * this is the case if the widget is not mounted yet, or is destroyed.
- */
- get el() {
- return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
- }
- /**
- * willStart is an asynchronous hook that can be implemented to perform some
- * action before the initial rendering of a component.
- *
- * It will be called exactly once before the initial rendering. It is useful
- * in some cases, for example, to load external assets (such as a JS library)
- * before the widget is rendered.
- *
- * Note that a slow willStart method will slow down the rendering of the user
- * interface. Therefore, some effort should be made to make this method as
- * fast as possible.
- *
- * Note: this method should not be called manually.
- */
- async willStart() { }
- /**
- * mounted is a hook that is called each time a component is attached to the
- * DOM. This is a good place to add some listeners, or to interact with the
- * DOM, if the component needs to perform some measure for example.
- *
- * Note: this method should not be called manually.
- *
- * @see willUnmount
- */
- mounted() { }
- /**
- * The willUpdateProps is an asynchronous hook, called just before new props
- * are set. This is useful if the component needs some asynchronous task
- * performed, depending on the props (for example, assuming that the props are
- * some record Id, fetching the record data).
- *
- * This hook is not called during the first render (but willStart is called
- * and performs a similar job).
- */
- async willUpdateProps(nextProps) { }
- /**
- * The willPatch hook is called just before the DOM patching process starts.
- * It is not called on the initial render. This is useful to get some
- * information which are in the DOM. For example, the current position of the
- * scrollbar
- *
- * The return value of willPatch will be given to the patched function.
- */
- willPatch() { }
- /**
- * This hook is called whenever a component did actually update its props,
- * state or env.
- *
- * This method is not called on the initial render. It is useful to interact
- * with the DOM (for example, through an external library) whenever the
- * component was updated.
- *
- * Updating the widget state in this hook is possible, but not encouraged.
- * One need to be careful, because updates here will cause rerender, which in
- * turn will cause other calls to updated. So, we need to be particularly
- * careful at avoiding endless cycles.
- *
- * The snapshot parameter is the result of the call to willPatch.
- */
- patched(snapshot) { }
- /**
- * willUnmount is a hook that is called each time just before a component is
- * unmounted from the DOM. This is a good place to remove some listeners, for
- * example.
- *
- * Note: this method should not be called manually.
- *
- * @see mounted
- */
- willUnmount() { }
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
- /**
- * Mount the component to a target element.
- *
- * This should only be done if the component was created manually. Components
- * created declaratively in templates are managed by the Owl system.
- */
- async mount(target) {
- const vnode = await this._prepare();
- if (this.__owl__.isDestroyed) {
- // component was destroyed before we get here...
- return;
- }
- this._patch(vnode);
- target.appendChild(this.el);
- if (document.body.contains(target)) {
- this._callMounted();
- }
- }
- unmount() {
- if (this.__owl__.isMounted) {
- this._callWillUnmount();
- this.el.remove();
- }
- }
- async render(force = false, patchQueue) {
- const __owl__ = this.__owl__;
- if (!__owl__.isMounted) {
- return;
- }
- const shouldPatch = !patchQueue;
- if (shouldPatch) {
- patchQueue = [];
- }
- const renderVDom = this._render(force, patchQueue);
- const renderId = __owl__.renderId;
- await renderVDom;
- if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
- // we only update the vnode and the actual DOM if no other rendering
- // occurred between now and when the render method was initially called.
- const patchLen = patchQueue.length;
- for (let i = 0; i < patchLen; i++) {
- const patch = patchQueue[i];
- patch.push(patch[0].willPatch());
- }
- for (let i = 0; i < patchLen; i++) {
- const patch = patchQueue[i];
- patch[0]._patch(patch[1]);
- }
- for (let i = patchLen - 1; i >= 0; i--) {
- const patch = patchQueue[i];
- patch[0].patched(patch[2]);
- }
- }
- }
- /**
- * Destroy the component. This operation is quite complex:
- * - it recursively destroy all children
- * - call the willUnmount hooks if necessary
- * - remove the dom node from the dom
- *
- * This should only be called manually if you created the widget. Most widgets
- * will be automatically destroyed.
- */
- destroy() {
- const __owl__ = this.__owl__;
- if (!__owl__.isDestroyed) {
- const el = this.el;
- this._destroy(__owl__.parent);
- if (el) {
- el.remove();
- }
- }
- }
- /**
- * This method is called by the component system whenever its props are
- * updated. If it returns true, then the component will be rendered.
- * Otherwise, it will skip the rendering (also, its props will not be updated)
- */
- shouldUpdate(nextProps) {
- return true;
- }
- /**
- * This method is the correct way to update the environment of a widget. Doing
- * this will cause a full rerender of the widget and its children, so this is
- * an operation that should not be done frequently.
- *
- * A good usecase for updating the environment would be to update some mostly
- * static config keys, such as a boolean to determine if we are in mobile
- * mode or not.
- */
- async updateEnv(nextEnv) {
- const __owl__ = this.__owl__;
- if (__owl__.parent && __owl__.parent.env === this.env) {
- this.env = Object.create(this.env);
- }
- Object.assign(this.env, nextEnv);
- if (__owl__.isMounted) {
- await this.render(true);
- }
- }
- /**
- * Emit a custom event of type 'eventType' with the given 'payload' on the
- * component's el, if it exists. However, note that the event will only bubble
- * up to the parent DOM nodes. Thus, it must be called between mounted() and
- * willUnmount().
- */
- trigger(eventType, payload) {
- if (this.el) {
- const ev = new CustomEvent(eventType, {
- bubbles: true,
- cancelable: true,
- detail: payload
- });
- this.el.dispatchEvent(ev);
- }
- }
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- _destroy(parent) {
- const __owl__ = this.__owl__;
- const isMounted = __owl__.isMounted;
- if (isMounted) {
- this.willUnmount();
- __owl__.isMounted = false;
- }
- const children = __owl__.children;
- for (let key in children) {
- children[key]._destroy(this);
- }
- if (parent) {
- let id = __owl__.id;
- delete parent.__owl__.children[id];
- __owl__.parent = null;
- }
- __owl__.isDestroyed = true;
- delete __owl__.vnode;
- }
- _callMounted() {
- const __owl__ = this.__owl__;
- const children = __owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (!comp.__owl__.isMounted && this.el.contains(comp.el)) {
- comp._callMounted();
- }
- }
- __owl__.isMounted = true;
- const handlers = __owl__.mountedHandlers;
- for (let key in handlers) {
- handlers[key]();
- }
- this.mounted();
- }
- _callWillUnmount() {
- this.willUnmount();
- const __owl__ = this.__owl__;
- __owl__.isMounted = false;
- const children = __owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (comp.__owl__.isMounted) {
- comp._callWillUnmount();
- }
- }
- }
- async _updateProps(nextProps, forceUpdate = false, patchQueue) {
- const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
- if (shouldUpdate) {
- const defaultProps = this.constructor.defaultProps;
- if (defaultProps) {
- nextProps = this._applyDefaultProps(nextProps, defaultProps);
- }
- if (QWeb.dev) {
- this._validateProps(nextProps);
- }
- await this.willUpdateProps(nextProps);
- this.props = nextProps;
- await this.render(forceUpdate, patchQueue);
- }
- }
- _patch(vnode) {
- const __owl__ = this.__owl__;
- __owl__.renderPromise = null;
- const target = __owl__.vnode || document.createElement(vnode.sel);
- __owl__.vnode = patch(target, vnode);
- }
- _prepare() {
- const __owl__ = this.__owl__;
- __owl__.renderProps = this.props;
- __owl__.renderPromise = this._prepareAndRender();
- return __owl__.renderPromise;
- }
- async _prepareAndRender() {
- await this.willStart();
- const __owl__ = this.__owl__;
- if (__owl__.isDestroyed) {
- return Promise.resolve(h("div"));
- }
- const qweb = this.env.qweb;
- if (!this.template) {
- let tmap = TEMPLATE_MAP[qweb.id];
- if (!tmap) {
- tmap = {};
- TEMPLATE_MAP[qweb.id] = tmap;
- }
- let p = this.constructor;
- let name = p.name;
- let template = tmap[name];
- if (template) {
- this.template = template;
- }
- else {
- while ((template = p.name) &&
- !(template in qweb.templates) &&
- p !== Component) {
- p = p.__proto__;
- }
- if (p === Component) {
- this.template = "default";
- }
- else {
- tmap[name] = template;
- this.template = template;
- }
- }
- }
- __owl__.render = qweb.render.bind(qweb, this.template);
- this._observeState();
- return this._render();
- }
- async _render(force = false, patchQueue = []) {
- const __owl__ = this.__owl__;
- __owl__.renderId++;
- const promises = [];
- const patch = [this];
- if (__owl__.isMounted) {
- patchQueue.push(patch);
- }
- if (__owl__.observer) {
- __owl__.observer.allowMutations = false;
- }
- let vnode = __owl__.render(this, {
- promises,
- handlers: __owl__.boundHandlers,
- mountedHandlers: __owl__.mountedHandlers,
- forceUpdate: force,
- patchQueue
- });
- patch.push(vnode);
- if (__owl__.observer) {
- __owl__.observer.allowMutations = true;
- }
- // this part is critical for the patching process to be done correctly. The
- // tricky part is that a child widget can be rerendered on its own, which
- // will update its own vnode representation without the knowledge of the
- // parent widget. With this, we make sure that the parent widget will be
- // able to patch itself properly after
- vnode.key = __owl__.id;
- __owl__.renderProps = this.props;
- __owl__.renderPromise = Promise.all(promises).then(() => vnode);
- return __owl__.renderPromise;
- }
- /**
- * Only called by qweb t-widget directive
- */
- _mount(vnode, elm) {
- const __owl__ = this.__owl__;
- __owl__.vnode = patch(elm, vnode);
- if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) {
- this._callMounted();
- }
- return __owl__.vnode;
- }
- /**
- * Only called by qweb t-widget directive (when t-keepalive is set)
- */
- _remount() {
- const __owl__ = this.__owl__;
- if (!__owl__.isMounted) {
- __owl__.isMounted = true;
- this.mounted();
- }
- }
- _observeState() {
- if (this.state) {
- const __owl__ = this.__owl__;
- __owl__.observer = new Observer();
- __owl__.observer.observe(this.state);
- __owl__.observer.notifyCB = this.render.bind(this);
- }
- }
- /**
- * Apply default props (only top level).
- *
- * Note that this method does not modify in place the props, it returns a new
- * prop object
- */
- _applyDefaultProps(props, defaultProps) {
- props = props ? Object.create(props) : {};
- for (let propName in defaultProps) {
- if (props[propName] === undefined) {
- props[propName] = defaultProps[propName];
- }
- }
- return props;
- }
- /**
- * Validate the component props (or next props) against the (static) props
- * description. This is potentially an expensive operation: it may needs to
- * visit recursively the props and all the children to check if they are valid.
- * This is why it is only done in 'dev' mode.
- */
- _validateProps(props) {
- const propsDef = this.constructor.props;
- if (propsDef instanceof Array) {
- // list of strings (prop names)
- for (let i = 0, l = propsDef.length; i < l; i++) {
- if (!(propsDef[i] in props)) {
- throw new Error(`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`);
- }
- }
- }
- else if (propsDef) {
- // propsDef is an object now
- for (let propName in propsDef) {
- if (!(propName in props)) {
- if (propsDef[propName] && !propsDef[propName].optional) {
- throw new Error(`Missing props '${propName}' (widget '${this.constructor.name}')`);
- }
- else {
- break;
- }
- }
- let isValid = isValidProp(props[propName], propsDef[propName]);
- if (!isValid) {
- throw new Error(`Props '${propName}' of invalid type in widget '${this.constructor.name}'`);
- }
- }
- }
- }
- }
- //------------------------------------------------------------------------------
- // Prop validation helper
- //------------------------------------------------------------------------------
- /**
- * Check if an invidual prop value matches its (static) prop definition
- */
- function isValidProp(prop, propDef) {
- if (typeof propDef === "function") {
- // Check if a value is constructed by some Constructor. Note that there is a
- // slight abuse of language: we want to consider primitive values as well.
- //
- // So, even though 1 is not an instance of Number, we want to consider that
- // it is valid.
- if (typeof prop === "object") {
- return prop instanceof propDef;
- }
- return typeof prop === propDef.name.toLowerCase();
- }
- else if (propDef instanceof Array) {
- // If this code is executed, this means that we want to check if a prop
- // matches at least one of its descriptor.
- let result = false;
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
- result = result || isValidProp(prop, propDef[i]);
- }
- return result;
- }
- // propsDef is an object
- let result = isValidProp(prop, propDef.type);
- if (propDef.type === Array) {
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
- result = result && isValidProp(prop[i], propDef.element);
- }
- }
- if (propDef.type === Object) {
- const shape = propDef.shape;
- for (let key in shape) {
- result = result && isValidProp(prop[key], shape[key]);
- }
- }
- return result;
- }
-
- /**
- * We define here a simple event bus: it can
- * - emit events
- * - add/remove listeners.
- *
- * This is a useful pattern of communication in many cases. For OWL, each
- * components and stores are event buses.
- */
- //------------------------------------------------------------------------------
- // EventBus
- //------------------------------------------------------------------------------
- class EventBus {
- constructor() {
- this.subscriptions = {};
- }
- /**
- * Add a listener for the 'eventType' events.
- *
- * Note that the 'owner' of this event can be anything, but will more likely
- * be a widget or a class. The idea is that the callback will be called with
- * the proper owner bound.
- *
- * Also, the owner should be kind of unique. This will be used to remove the
- * listener.
- */
- on(eventType, owner, callback) {
- if (!callback) {
- throw new Error("Missing callback");
- }
- if (!this.subscriptions[eventType]) {
- this.subscriptions[eventType] = [];
- }
- this.subscriptions[eventType].push({
- owner,
- callback
- });
- }
- /**
- * Remove a listener
- */
- off(eventType, owner) {
- const subs = this.subscriptions[eventType];
- if (subs) {
- this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
- }
- }
- /**
- * Emit an event of type 'eventType'. Any extra arguments will be passed to
- * the listeners callback.
- */
- trigger(eventType, ...args) {
- const subs = this.subscriptions[eventType] || [];
- for (let i = 0, iLen = subs.length; i < iLen; i++) {
- const sub = subs[i];
- sub.callback.call(sub.owner, ...args);
- }
- }
- /**
- * Remove all subscriptions.
- */
- clear() {
- this.subscriptions = {};
- }
- }
-
- /**
- * Owl QWeb Directives
- *
- * This file contains the implementation of most standard QWeb directives:
- * - t-esc
- * - t-raw
- * - t-set/t-value
- * - t-if/t-elif/t-else
- * - t-call
- * - t-foreach/t-as
- * - t-debug
- * - t-log
- */
- //------------------------------------------------------------------------------
- // t-esc and t-raw
- //------------------------------------------------------------------------------
- UTILS.getFragment = function (str) {
- const temp = document.createElement("template");
- temp.innerHTML = str;
- return temp.content;
- };
- function compileValueNode(value, node, qweb, ctx) {
- if (value === "0" && ctx.caller) {
- qweb._compileNode(ctx.caller, ctx);
- return;
- }
- if (value.xml instanceof NodeList) {
- for (let node of Array.from(value.xml)) {
- qweb._compileNode(node, ctx);
- }
- return;
- }
- let exprID;
- if (typeof value === "string") {
- exprID = `_${ctx.generateID()}`;
- ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
- }
- else {
- exprID = value.id;
- }
- ctx.addIf(`${exprID} || ${exprID} === 0`);
- if (ctx.escaping) {
- if (ctx.parentTextNode) {
- ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
- }
- else if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
- }
- else {
- let nodeID = ctx.generateID();
- ctx.rootContext.rootNode = nodeID;
- ctx.rootContext.parentTextNode = nodeID;
- ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
- }
- }
- else {
- let fragID = ctx.generateID();
- ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
- let tempNodeID = ctx.generateID();
- ctx.addLine(`var p${tempNodeID} = {hook: {`);
- ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
- ctx.addLine(`}};`);
- ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
- ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
- }
- if (node.childNodes.length) {
- ctx.addElse();
- qweb._compileChildren(node, ctx);
- }
- ctx.closeIf();
- }
- QWeb.addDirective({
- name: "esc",
- priority: 70,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-esc"));
- compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
- return true;
- }
- });
- QWeb.addDirective({
- name: "raw",
- priority: 80,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-raw"));
- compileValueNode(value, node, qweb, ctx);
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-set
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "set",
- extraNames: ["value"],
- priority: 60,
- atNodeEncounter({ node, ctx }) {
- const variable = node.getAttribute("t-set");
- let value = node.getAttribute("t-value");
- if (value) {
- const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable)) {
- ctx.addLine(`${ctx.variables[variable].id} = ${formattedValue}`);
- }
- else {
- const varName = `_${ctx.generateID()}`;
- ctx.addLine(`var ${varName} = ${formattedValue};`);
- ctx.variables[variable] = {
- id: varName,
- expr: formattedValue
- };
- }
- }
- else {
- ctx.variables[variable] = {
- xml: node.childNodes
- };
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-if, t-elif, t-else
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "if",
- priority: 20,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-if"));
- ctx.addIf(`${ctx.formatExpression(cond)}`);
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- QWeb.addDirective({
- name: "elif",
- priority: 30,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-elif"));
- ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- QWeb.addDirective({
- name: "else",
- priority: 40,
- atNodeEncounter({ ctx }) {
- ctx.addLine(`else {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- });
- //------------------------------------------------------------------------------
- // t-call
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "call",
- priority: 50,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- throw new Error("Invalid tag for t-call directive (should be 't')");
- }
- const subTemplate = node.getAttribute("t-call");
- const nodeTemplate = qweb.templates[subTemplate];
- if (!nodeTemplate) {
- throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
- }
- const nodeCopy = node.cloneNode(true);
- nodeCopy.removeAttribute("t-call");
- // extract variables from nodecopy
- const tempCtx = new Context();
- tempCtx.nextID = ctx.rootContext.nextID;
- qweb._compileNode(nodeCopy, tempCtx);
- const vars = Object.assign({}, ctx.variables, tempCtx.variables);
- ctx.rootContext.nextID = tempCtx.nextID;
- // open new scope, if necessary
- const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
- if (hasNewVariables) {
- ctx.addLine("{");
- ctx.indent();
- // add new variables, if any
- for (let key in tempCtx.variables) {
- const v = tempCtx.variables[key];
- if (v.expr) {
- ctx.addLine(`let ${v.id} = ${v.expr};`);
- }
- // todo: handle XML variables...
- }
- }
- // compile sub template
- const subCtx = ctx
- .subContext("caller", nodeCopy)
- .subContext("variables", Object.create(vars));
- qweb._compileNode(nodeTemplate.elem, subCtx);
- // close new scope
- if (hasNewVariables) {
- ctx.dedent();
- ctx.addLine("}");
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-foreach
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "foreach",
- extraNames: ["as"],
- priority: 10,
- atNodeEncounter({ node, qweb, ctx }) {
- ctx.rootContext.shouldProtectContext = true;
- ctx = ctx.subContext("inLoop", true);
- const elems = node.getAttribute("t-foreach");
- const name = node.getAttribute("t-as");
- let arrayID = ctx.generateID();
- ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
- ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
- let keysID = ctx.generateID();
- let valuesID = ctx.generateID();
- ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
- ctx.addIf(`!(_${arrayID} instanceof Array)`);
- ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
- ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
- ctx.closeIf();
- ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
- ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
- ctx.indent();
- ctx.addLine(`context.${name}_first = i === 0;`);
- ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
- ctx.addLine(`context.${name}_index = i;`);
- ctx.addLine(`context.${name} = _${keysID}[i];`);
- ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
- const nodeCopy = node.cloneNode(true);
- let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
- if (!shouldWarn && node.tagName === "t") {
- if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
- shouldWarn = true;
- }
- if (!shouldWarn &&
- node.children.length === 1 &&
- node.children[0].tagName !== "t" &&
- !node.children[0].hasAttribute("t-key")) {
- shouldWarn = true;
- }
- }
- if (shouldWarn) {
- console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`);
- }
- nodeCopy.removeAttribute("t-foreach");
- qweb._compileNode(nodeCopy, ctx);
- ctx.dedent();
- ctx.addLine("}");
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-debug
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "debug",
- priority: 99,
- atNodeEncounter({ ctx }) {
- ctx.addLine("debugger;");
- }
- });
- //------------------------------------------------------------------------------
- // t-log
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "log",
- priority: 99,
- atNodeEncounter({ ctx, value }) {
- const expr = ctx.formatExpression(value);
- ctx.addLine(`console.log(${expr})`);
- }
- });
-
- /**
- * Owl QWeb Extensions
- *
- * This file contains the implementation of non standard QWeb directives, added
- * by Owl and that will only work on Owl projects:
- *
- * - t-on
- * - t-ref
- * - t-transition
- * - t-widget/t-keepalive
- * - t-mounted
- * - t-slot
- */
- //------------------------------------------------------------------------------
- // t-on
- //------------------------------------------------------------------------------
- // these are pieces of code that will be injected into the event handler if
- // modifiers are specified
- const MODS_CODE = {
- prevent: "e.preventDefault();",
- self: "if (e.target !== this.elm) {return}",
- stop: "e.stopPropagation();"
- };
- QWeb.addDirective({
- name: "on",
- priority: 90,
- atNodeCreation({ ctx, fullName, value, nodeID }) {
- ctx.rootContext.shouldDefineOwner = true;
- const [eventName, ...mods] = fullName.slice(5).split(".");
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handlerName = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- ctx.addIf(`!context['${handlerName}']`);
- ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`);
- ctx.closeIf();
- let params = extraArgs
- ? `owner, ${ctx.formatExpression(extraArgs)}`
- : "owner";
- let handler;
- if (mods.length > 0) {
- handler = `function (e) {`;
- handler += mods
- .map(function (mod) {
- return MODS_CODE[mod];
- })
- .join("");
- handler += `context['${handlerName}'].call(${params}, e);}`;
- }
- else {
- handler = `context['${handlerName}'].bind(${params})`;
- }
- if (extraArgs) {
- ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
- }
- else {
- ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`);
- ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
- }
- }
- });
- //------------------------------------------------------------------------------
- // t-ref
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "ref",
- priority: 95,
- atNodeCreation({ ctx, value, addNodeHook }) {
- const refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
- addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
- }
- });
- //------------------------------------------------------------------------------
- // t-transition
- //------------------------------------------------------------------------------
- UTILS.nextFrame = function (cb) {
- requestAnimationFrame(() => requestAnimationFrame(cb));
- };
- UTILS.transitionInsert = function (vn, name) {
- const elm = vn.elm;
- // remove potential duplicated vnode that is currently being removed, to
- // prevent from having twice the same node in the DOM during an animation
- const dup = elm.parentElement &&
- elm.parentElement.querySelector(`*[data-owl-key='${vn.key}']`);
- if (dup) {
- dup.remove();
- }
- elm.classList.add(name + "-enter");
- elm.classList.add(name + "-enter-active");
- const finalize = () => {
- elm.classList.remove(name + "-enter-active");
- elm.classList.remove(name + "-enter-to");
- };
- this.nextFrame(() => {
- elm.classList.remove(name + "-enter");
- elm.classList.add(name + "-enter-to");
- whenTransitionEnd(elm, finalize);
- });
- };
- UTILS.transitionRemove = function (vn, name, rm) {
- const elm = vn.elm;
- elm.setAttribute("data-owl-key", vn.key);
- elm.classList.add(name + "-leave");
- elm.classList.add(name + "-leave-active");
- const finalize = () => {
- elm.classList.remove(name + "-leave-active");
- elm.classList.remove(name + "-leave-to");
- rm();
- };
- this.nextFrame(() => {
- elm.classList.remove(name + "-leave");
- elm.classList.add(name + "-leave-to");
- whenTransitionEnd(elm, finalize);
- });
- };
- function getTimeout(delays, durations) {
- /* istanbul ignore next */
- while (delays.length < durations.length) {
- delays = delays.concat(delays);
- }
- return Math.max.apply(null, durations.map((d, i) => {
- return toMs(d) + toMs(delays[i]);
- }));
- }
- // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
- // in a locale-dependent way, using a comma instead of a dot.
- // If comma is not replaced with a dot, the input will be rounded down (i.e. acting
- // as a floor function) causing unexpected behaviors
- function toMs(s) {
- return Number(s.slice(0, -1).replace(",", ".")) * 1000;
- }
- function whenTransitionEnd(elm, cb) {
- const styles = window.getComputedStyle(elm);
- const delays = (styles.transitionDelay || "").split(", ");
- const durations = (styles.transitionDuration || "").split(", ");
- const timeout = getTimeout(delays, durations);
- if (timeout > 0) {
- elm.addEventListener("transitionend", cb, { once: true });
- }
- else {
- cb();
- }
- }
- QWeb.addDirective({
- name: "transition",
- priority: 96,
- atNodeCreation({ value, addNodeHook }) {
- let name = value;
- const hooks = {
- insert: `this.utils.transitionInsert(vn, '${name}');`,
- remove: `this.utils.transitionRemove(vn, '${name}', rm);`
- };
- for (let hookName in hooks) {
- addNodeHook(hookName, hooks[hookName]);
- }
- }
- });
- //------------------------------------------------------------------------------
- // t-widget
- //------------------------------------------------------------------------------
- const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
- self: "if (e.target !== vn.elm) {return}"
- });
- /**
- * The t-widget directive is certainly a complicated and hard to maintain piece
- * of code. To help you, fellow developer, if you have to maintain it, I offer
- * you this advice: Good luck...
- *
- * Since it is not 'direct' code, but rather code that generates other code, it
- * is not easy to understand. To help you, here is a detailed and commented
- * explanation of the code generated by the t-widget directive for the following
- * situation:
- * ```xml
- *
- * ```
- *
- * ```js
- * // we assign utils on top of the function because it will be useful for
- * // each widgets
- * let utils = this.utils;
- *
- * // this is the virtual node representing the parent div
- * let c1 = [], p1 = { key: 1 };
- * var vn1 = h("div", p1, c1);
- *
- * // t-widget directive: we start by evaluating the expression given by t-key:
- * let key5 = "somestring";
- *
- * // We keep the index of the position of the widget in the closure. We push
- * // null to reserve the slot, and will replace it later by the widget vnode,
- * // when it will be ready (do not forget that preparing/rendering a widget is
- * // asynchronous)
- * let _2_index = c1.length;
- * c1.push(null);
- *
- * // def3 is the promise that will contain later either the new widget
- * // creation, or the props update...
- * let def3;
- *
- * // this is kind of tricky: we need here to find if the widget was already
- * // created by a previous rendering. This is done by checking the internal
- * // `cmap` (children map) of the parent widget: it maps keys to widget ids,
- * // and, then, if there is an id, we look into the children list to get the
- * // instance
- * let w4 =
- * key5 in context.__owl__.cmap
- * ? context.__owl__.children[context.__owl__.cmap[key5]]
- * : false;
- *
- * // we evaluate here the props given to the component. It is done here to be
- * // able to easily reference it later, and also, it might be an expensive
- * // computation, so it is certainly better to do it only once
- * let props4 = { flag: context["state"].flag };
- *
- * // If we have a widget, currently rendering, but not ready yet, we do not want
- * // to wait for it to be ready if we can avoid it
- * if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
- * // we check if the props are the same. In that case, we can simply reuse
- * // the previous rendering and skip all useless work
- * if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
- * def3 = w4.__owl__.renderPromise;
- * } else {
- * // if the props are not the same, we destroy the widget and starts anew.
- * // this will be faster than waiting for its rendering, then updating it
- * w4.destroy();
- * w4 = false;
- * }
- * }
- *
- * if (!w4) {
- * // in this situation, we need to create a new widget. First step is
- * // to get a reference to the class, then create an instance with
- * // current context as parent, and the props.
- * let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
-
- * if (!W4) {
- * throw new Error("Cannot find the definition of widget 'child'");
- * }
- * w4 = new W4(owner, props4);
- *
- * // Whenever we rerender the parent widget, we need to be sure that we
- * // are able to find the widget instance. To do that, we register it to
- * // the parent cmap (children map). Note that the 'template' key is
- * // used here, since this is what identify the widget from the template
- * // perspective.
- * context.__owl__.cmap[key5] = w4.__owl__.id;
- *
- * // _prepare is called, to basically call willStart, then render the
- * // widget
- * def3 = w4._prepare();
- *
- * def3 = def3.then(vnode => {
- * // we create here a virtual node for the parent (NOT the widget). This
- * // means that the vdom of the parent will be stopped here, and from
- * // the parent's perspective, it simply is a vnode with no children.
- * // However, it shares the same dom element with the component root
- * // vnode.
- * let pvnode = h(vnode.sel, { key: key5 });
- *
- * // we add hooks to the parent vnode so we can interact with the new
- * // widget at the proper time
- * pvnode.data.hook = {
- * insert(vn) {
- * // the _mount method will patch the widget vdom into the elm vn.elm,
- * // then call the mounted hooks. However, suprisingly, the snabbdom
- * // patch method actually replace the elm by a new elm, so we need
- * // to synchronise the pvnode elm with the resulting elm
- * let nvn = w4._mount(vnode, vn.elm);
- * pvnode.elm = nvn.elm;
- * // what follows is only present if there are animations on the widget
- * utils.transitionInsert(vn, "fade");
- * },
- * remove() {
- * // override with empty function to prevent from removing the node
- * // directly. It will be removed when destroy is called anyway, which
- * // delays the removal if there are animations.
- * },
- * destroy() {
- * // if there are animations, we delay the call to destroy on the
- * // widget, if not, we call it directly.
- * let finalize = () => {
- * w4.destroy();
- * };
- * utils.transitionRemove(vn, "fade", finalize);
- * }
- * };
- * // the pvnode is inserted at the correct position in the div's children
- * c1[_2_index] = pvnode;
- *
- * // we keep here a reference to the parent vnode (representing the
- * // widget, so we can reuse it later whenever we update the widget
- * w4.__owl__.pvnode = pvnode;
- * });
- * } else {
- * // this is the 'update' path of the directive.
- * // the call to _updateProps is the actual widget update
- * // Note that we only update the props if we cannot reuse the previous
- * // rendering work (in the case it was rendered with the same props)
- * def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
- * def3 = def3.then(() => {
- * // if widget was destroyed in the meantime, we do nothing (so, this
- * // means that the parent's element children list will have a null in
- * // the widget's position, which will cause the pvnode to be removed
- * // when it is patched.
- * if (w4.__owl__.isDestroyed) {
- * return;
- * }
- * // like above, we register the pvnode to the children list, so it
- * // will not be patched out of the dom.
- * let pvnode = w4.__owl__.pvnode;
- * c1[_2_index] = pvnode;
- * });
- * }
- *
- * // we register the deferred here so the parent can coordinate its patch operation
- * // with all the children.
- * extra.promises.push(def3);
- * return vn1;
- * ```
- */
- QWeb.addDirective({
- name: "widget",
- extraNames: ["props", "keepalive"],
- priority: 100,
- atNodeEncounter({ ctx, value, node, qweb }) {
- ctx.addLine("//WIDGET");
- ctx.rootContext.shouldDefineOwner = true;
- ctx.rootContext.shouldDefineQWeb = true;
- ctx.rootContext.shouldDefineUtils = true;
- let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- // t-on- events and t-transition
- const events = [];
- let transition = "";
- const attributes = node.attributes;
- const props = {};
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- const value = attributes[i].textContent;
- if (name.startsWith("t-on-")) {
- const [eventName, ...mods] = name.slice(5).split(".");
- let extraArgs;
- let handlerName = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- events.push([eventName, mods, handlerName, extraArgs]);
- }
- else if (name === "t-transition") {
- transition = value;
- }
- else if (!name.startsWith("t-")) {
- if (name !== "class" && name !== "style") {
- // this is a prop!
- props[name] = ctx.formatExpression(value);
- }
- }
- }
- let key = node.getAttribute("t-key");
- if (key) {
- key = ctx.formatExpression(key);
- }
- // computing the props string representing the props object
- let propStr = Object.keys(props)
- .map(k => k + ":" + props[k])
- .join(",");
- let dummyID = ctx.generateID();
- let defID = ctx.generateID();
- let widgetID = ctx.generateID();
- let keyID = key && ctx.generateID();
- if (key) {
- // we bind a variable to the key (could be a complex expression, so we
- // want to evaluate it only once)
- ctx.addLine(`let key${keyID} = ${key};`);
- }
- ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
- ctx.addLine(`c${ctx.parentNode}.push(null);`);
- ctx.addLine(`let def${defID};`);
- let templateID = key
- ? `key${keyID}`
- : ctx.inLoop
- ? `String(-${widgetID} - i)`
- : String(widgetID);
- let ref = node.getAttribute("t-ref");
- let refExpr = "";
- let refKey = "";
- if (ref) {
- refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
- refExpr = `context.refs[${refKey}] = w${widgetID};`;
- }
- let transitionsInsertCode = "";
- if (transition) {
- transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
- }
- let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}();`;
- if (ref && !keepAlive) {
- finalizeWidgetCode += `delete context.refs[${refKey}];`;
- }
- if (transition) {
- finalizeWidgetCode = `let finalize = () => {
- ${finalizeWidgetCode}
- };
- utils.transitionRemove(vn, '${transition}', finalize);`;
- }
- let createHook = "";
- let classAttr = node.getAttribute("class");
- let tattClass = node.getAttribute("t-att-class");
- let styleAttr = node.getAttribute("style");
- let tattStyle = node.getAttribute("t-att-style");
- if (tattStyle) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
- tattStyle = attVar;
- }
- let updateClassCode = "";
- if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
- let classCode = "";
- if (classAttr) {
- classCode =
- classAttr
- .split(" ")
- .map(c => `vn.elm.classList.add('${c}')`)
- .join(";") + ";";
- }
- if (tattClass) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
- classCode = `for (let k in ${attVar}) {
- if (${attVar}[k]) {
- vn.elm.classList.add(k);
- }
- }`;
- updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
- }
- let eventsCode = events
- .map(function ([eventName, mods, handlerName, extraArgs]) {
- let params = extraArgs
- ? `owner, ${ctx.formatExpression(extraArgs)}`
- : "owner";
- let handler;
- if (mods.length > 0) {
- handler = `function (e) {`;
- handler += mods
- .map(function (mod) {
- return T_WIDGET_MODS_CODE[mod];
- })
- .join("");
- handler += `owner['${handlerName}'].call(${params}, e);}`;
- }
- else {
- handler = `owner['${handlerName}'].bind(${params})`;
- }
- return `vn.elm.addEventListener('${eventName}', ${handler});`;
- })
- .join("");
- const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
- const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
- createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
- }
- ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`);
- ctx.addLine(`let props${widgetID} = {${propStr}};`);
- ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`);
- ctx.addIf(`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`);
- ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
- ctx.addElse();
- ctx.addLine(`w${widgetID}.destroy();`);
- ctx.addLine(`w${widgetID} = false;`);
- ctx.closeIf();
- ctx.closeIf();
- ctx.addIf(`!w${widgetID}`);
- // new widget
- ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
- ctx.addLine(`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`);
- // maybe only do this in dev mode...
- ctx.addLine(`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`);
- ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
- ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`);
- // SLOTS
- const slotNodes = node.querySelectorAll("[t-set]");
- if (slotNodes.length) {
- const slotId = qweb.nextSlotId++;
- for (let i = 0, length = slotNodes.length; i < length; i++) {
- const slotNode = slotNodes[i];
- const key = slotNode.getAttribute("t-set");
- slotNode.removeAttribute("t-set");
- const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
- qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
- }
- ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`);
- }
- ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
- // hack: specify empty remove hook to prevent the node from being removed from the DOM
- // FIXME: click to re-add widget during remove transition -> leak
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`);
- ctx.addElse();
- // need to update widget
- ctx.addLine(`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`);
- let keepAliveCode = "";
- if (keepAlive) {
- keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
- }
- ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`);
- ctx.closeIf();
- ctx.addLine(`extra.promises.push(def${defID});`);
- if (node.hasAttribute("t-if") ||
- node.hasAttribute("t-else") ||
- node.hasAttribute("t-elif")) {
- ctx.closeIf();
- }
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-mounted
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "mounted",
- priority: 97,
- atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
- ctx.rootContext.shouldDefineOwner = true;
- const eventName = fullName.slice(5);
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handler = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`;
- if (extraArgs) {
- ctx.addLine(`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`);
- }
- else {
- ctx.addLine(`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`);
- }
- addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
- }
- });
- //------------------------------------------------------------------------------
- // t-slot
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "slot",
- priority: 80,
- atNodeEncounter({ ctx, value }) {
- const slotKey = ctx.generateID();
- ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
- ctx.addLine(`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`);
- return true;
- }
- });
-
- class Store extends EventBus {
- constructor(config, options = {}) {
- super();
- this._commitLevel = 0;
- this.history = [];
- this.debug = options.debug || false;
- this.state = config.state || {};
- this.actions = config.actions;
- this.mutations = config.mutations;
- this.env = config.env;
- this.observer = new Observer();
- this.observer.notifyCB = this.trigger.bind(this, "update");
- this.observer.allowMutations = false;
- this.observer.observe(this.state);
- this.getters = {};
- if (this.debug) {
- this.history.push({ state: this.state });
- }
- for (let entry of Object.entries(config.getters || {})) {
- const name = entry[0];
- const func = entry[1];
- this.getters[name] = payload => {
- return func({ state: this.state, getters: this.getters }, payload);
- };
- }
- }
- dispatch(action, payload) {
- if (!this.actions[action]) {
- throw new Error(`[Error] action ${action} is undefined`);
- }
- const result = this.actions[action]({
- commit: this.commit.bind(this),
- dispatch: this.dispatch.bind(this),
- env: this.env,
- state: this.state,
- getters: this.getters
- }, payload);
- if (result instanceof Promise) {
- return new Promise((resolve, reject) => {
- result.then(() => resolve());
- result.catch(reject);
- });
- }
- }
- commit(type, payload) {
- if (!this.mutations[type]) {
- throw new Error(`[Error] mutation ${type} is undefined`);
- }
- this._commitLevel++;
- this.observer.allowMutations = true;
- const res = this.mutations[type].call(null, {
- commit: this.commit.bind(this),
- state: this.state,
- getters: this.getters
- }, payload);
- if (this._commitLevel === 1) {
- this.observer.allowMutations = false;
- if (this.debug) {
- this.history.push({
- state: this.state,
- mutation: type,
- payload: payload
- });
- }
- }
- this._commitLevel--;
- return res;
- }
- }
- //------------------------------------------------------------------------------
- // Connect function
- //------------------------------------------------------------------------------
- function revNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.rev;
- }
- return 0;
- }
- function deepRevNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.deepRev;
- }
- return 0;
- }
- let nextID$1 = 1;
- function connect(Comp, mapStoreToProps, options = {}) {
- let hashFunction = options.hashFunction || null;
- const getStore = options.getStore || (env => env.store);
- if (!hashFunction) {
- let deep = "deep" in options ? options.deep : true;
- let defaultRevFunction = deep ? deepRevNumber : revNumber;
- hashFunction = function ({ storeProps }, options) {
- const { currentStoreProps } = options;
- if ("__owl__" in storeProps) {
- return defaultRevFunction(storeProps);
- }
- let hash = 0;
- for (let key in storeProps) {
- const val = storeProps[key];
- const hashVal = defaultRevFunction(val);
- if (hashVal === 0) {
- if (val !== currentStoreProps[key]) {
- options.didChange = true;
- }
- }
- else {
- hash += hashVal;
- }
- }
- return hash;
- };
- }
- const Result = class extends Comp {
- constructor(parent, props) {
- const env = parent instanceof Component ? parent.env : parent;
- const store = getStore(env);
- const ownProps = Object.assign({}, props || {});
- const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
- const mergedProps = Object.assign({}, props || {}, storeProps);
- super(parent, mergedProps);
- this.__owl__.ownProps = ownProps;
- this.__owl__.currentStoreProps = storeProps;
- this.__owl__.store = store;
- this.__owl__.storeHash = hashFunction({
- state: store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, {
- currentStoreProps: storeProps
- });
- }
- /**
- * We do not use the mounted hook here for a subtle reason: we want the
- * updates to be called for the parents before the children. However,
- * if we use the mounted hook, this will be done in the reverse order.
- */
- _callMounted() {
- this.__owl__.store.on("update", this, this._checkUpdate);
- super._callMounted();
- }
- willUnmount() {
- this.__owl__.store.off("update", this);
- super.willUnmount();
- }
- _checkUpdate() {
- const ownProps = this.__owl__.ownProps;
- const storeProps = mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters);
- const options = {
- currentStoreProps: this.__owl__.currentStoreProps
- };
- const storeHash = hashFunction({
- state: this.__owl__.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, options);
- let didChange = options.didChange;
- if (storeHash !== this.__owl__.storeHash) {
- didChange = true;
- this.__owl__.storeHash = storeHash;
- }
- if (didChange) {
- this.__owl__.currentStoreProps = storeProps;
- this._updateProps(ownProps, false);
- }
- }
- _updateProps(nextProps, forceUpdate, patchQueue) {
- if (this.__owl__.ownProps !== nextProps) {
- this.__owl__.currentStoreProps = mapStoreToProps(this.__owl__.store.state, nextProps, this.__owl__.store.getters);
- }
- this.__owl__.ownProps = nextProps;
- const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps);
- return super._updateProps(mergedProps, forceUpdate, patchQueue);
- }
- };
- // we assign here a unique name to the resulting anonymous class.
- // this is necessary for Owl to be able to properly deduce templates.
- // Otherwise, all connected components would have the same name, and then
- // each component after the first will necessarily have the same template.
- let name = `ConnectedComponent${nextID$1++}`;
- Object.defineProperty(Result, "name", { value: name });
- return Result;
- }
-
- /**
- * Owl Utils
- *
- * We have here a small collection of utility functions:
- *
- * - whenReady
- * - loadJS
- * - loadTemplates
- * - escape
- * - debounce
- */
- function whenReady(fn) {
- return new Promise(function (resolve) {
- if (document.readyState !== "loading") {
- resolve();
- }
- else {
- document.addEventListener("DOMContentLoaded", resolve, false);
- }
- }).then(fn || function () { });
- }
- const loadedScripts = {};
- function loadJS(url) {
- if (url in loadedScripts) {
- return loadedScripts[url];
- }
- const promise = new Promise(function (resolve, reject) {
- const script = document.createElement("script");
- script.type = "text/javascript";
- script.src = url;
- script.onload = function () {
- resolve();
- };
- script.onerror = function () {
- reject(`Error loading file '${url}'`);
- };
- const head = document.head || document.getElementsByTagName("head")[0];
- head.appendChild(script);
- });
- loadedScripts[url] = promise;
- return promise;
- }
- async function loadTemplates(url) {
- const result = await fetch(url);
- if (!result.ok) {
- throw new Error("Error while fetching xml templates");
- }
- let templates = await result.text();
- templates = templates.replace(//g, "");
- return templates;
- }
- function escape(str) {
- if (str === undefined) {
- return "";
- }
- if (typeof str === "number") {
- return String(str);
- }
- return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, "'")
- .replace(/`/g, "`");
- }
- /**
- * Returns a function, that, as long as it continues to be invoked, will not
- * be triggered. The function will be called after it stops being called for
- * N milliseconds. If `immediate` is passed, trigger the function on the
- * leading edge, instead of the trailing.
- *
- * Inspired by https://davidwalsh.name/javascript-debounce-function
- */
- function debounce(func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- function later() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- }
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
- }
-
- var _utils = /*#__PURE__*/Object.freeze({
- whenReady: whenReady,
- loadJS: loadJS,
- loadTemplates: loadTemplates,
- escape: escape,
- debounce: debounce
- });
-
- /**
- * This file is the main file packaged by rollup (see rollup.config.js). From
- * this file, we export all public owl elements.
- *
- * Note that dynamic values, such as a date or a commit hash are added by rollup
- */
- const __info__ = {};
- Object.defineProperty(__info__, "mode", {
- get() {
- return QWeb.dev ? "dev" : "prod";
- },
- set(mode) {
- QWeb.dev = mode === "dev";
- if (QWeb.dev) {
- const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
- console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
- }
- else {
- console.log(`Owl is now running in 'prod' mode.`);
- }
- }
- });
- const utils = _utils;
-
- exports.Component = Component;
- exports.EventBus = EventBus;
- exports.Observer = Observer;
- exports.QWeb = QWeb;
- exports.Store = Store;
- exports.__info__ = __info__;
- exports.connect = connect;
- exports.utils = utils;
-
- exports.__info__.version = '0.14.0';
- exports.__info__.date = '2019-06-13T10:11:15.338Z';
- exports.__info__.hash = '2701a7d';
- exports.__info__.url = 'https://github.com/odoo/owl';
-
-}(this.owl = this.owl || {}));
diff --git a/tools/benchmarks/owl-0.14.0/templates.xml b/tools/benchmarks/owl-0.14.0/templates.xml
deleted file mode 100644
index b53a3dac..00000000
--- a/tools/benchmarks/owl-0.14.0/templates.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/benchmarks/owl-0.16.0/app.js b/tools/benchmarks/owl-0.16.0/app.js
deleted file mode 100644
index 945b1443..00000000
--- a/tools/benchmarks/owl-0.16.0/app.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
-
-//------------------------------------------------------------------------------
-// Likes Counter Widget
-//------------------------------------------------------------------------------
-class Counter extends owl.Component {
- state = { counter: 0 };
-
- increment() {
- this.state.counter++;
- }
-}
-
-//------------------------------------------------------------------------------
-// Message Widget
-//------------------------------------------------------------------------------
-class Message extends owl.Component {
- components = { Counter };
-
- shouldUpdate(nextProps) {
- return nextProps.message !== this.props.message;
- }
- removeMessage() {
- this.trigger("remove-message", {
- id: this.props.message.id
- });
- }
-}
-
-//------------------------------------------------------------------------------
-// Root Widget
-//------------------------------------------------------------------------------
-class App extends owl.Component {
- components = { Message };
- state = { messages: [], multipleFlag: false, clearAfterFlag: false };
-
- mounted() {
- this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
- }
-
- benchmark(message, fn, callback) {
- if (this.state.multipleFlag) {
- const N = 20;
- let n = N;
- let total = 0;
- let cb = info => {
- let finalize = () => {
- n--;
- total += info.delta;
- if (n === 0) {
- const avg = total / N;
- this.log(`Average: ${formatNumber(avg)}ms`, true);
- if (callback) {
- callback();
- }
- } else {
- this._benchmark(message, fn, cb);
- }
- };
-
- if (this.state.clearAfterFlag) {
- this._benchmark(
- "clear",
- () => {
- this.state.messages = [];
- },
- finalize,
- false
- );
- } else {
- finalize();
- }
- };
- this._benchmark(message, fn, cb);
- } else {
- this._benchmark(message, fn, callback);
- }
- }
-
- _benchmark(message, fn, cb, log = true) {
- setTimeout(() => {
- startMeasure(message);
- fn();
- stopMeasure(info => {
- if (log) {
- this.log(info.msg);
- }
- if (cb) {
- cb(info);
- }
- });
- }, 10);
- }
-
- addMessages(n) {
- this.benchmark("add " + n, () => {
- const newMessages = buildData(n);
- this.state.messages.push.apply(this.state.messages, newMessages);
- });
- }
-
- clear() {
- this._benchmark("clear", () => {
- this.state.messages = [];
- });
- }
-
- updateSomeMessages() {
- this.benchmark("update every 10th", () => {
- const messages = this.state.messages;
- for (let i = 0; i < this.state.messages.length; i += 10) {
- const msg = Object.assign({}, messages[i]);
- msg.author += "!!!";
- this.set(messages, i, msg);
- }
- });
- }
-
- removeMessage(event) {
- this.benchmark("remove message", () => {
- const index = this.state.messages.findIndex(m => m.id === event.detail.id);
- this.state.messages.splice(index, 1);
- });
- }
-
- log(str, isBold) {
- const div = document.createElement("div");
- if (isBold) {
- div.classList.add("bold");
- }
- div.textContent = `> ${str}`;
- this.refs.log.appendChild(div);
- this.refs.log.scrollTop = this.refs.log.scrollHeight;
- }
-
- clearLog() {
- this.refs.log.innerHTML = "";
- }
-
- toggleMultiple() {
- this.state.multipleFlag = !this.state.multipleFlag;
- }
-
- toggleClear() {
- this.state.clearAfterFlag = !this.state.clearAfterFlag;
- }
-}
-
-//------------------------------------------------------------------------------
-// Application initialization
-//------------------------------------------------------------------------------
-async function start() {
- const templates = await owl.utils.loadTemplates("templates.xml");
- const env = {
- qweb: new owl.QWeb(templates)
- };
- const app = new App(env);
- app.mount(document.body);
-}
-
-start();
diff --git a/tools/benchmarks/owl-0.16.0/index.html b/tools/benchmarks/owl-0.16.0/index.html
deleted file mode 100644
index 23e1f555..00000000
--- a/tools/benchmarks/owl-0.16.0/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
- OWL 0.16.0 Benchmark
-
-
-
-
-
-
-
-
diff --git a/tools/benchmarks/owl-0.8.0/app.js b/tools/benchmarks/owl-0.8.0/app.js
deleted file mode 100644
index f89a020a..00000000
--- a/tools/benchmarks/owl-0.8.0/app.js
+++ /dev/null
@@ -1,173 +0,0 @@
-import {
- buildData,
- startMeasure,
- stopMeasure,
- formatNumber
-} from "../shared/utils.js";
-
-//------------------------------------------------------------------------------
-// Likes Counter Widget
-//------------------------------------------------------------------------------
-class Counter extends owl.Component {
- state = { counter: 0 };
- template = "Counter";
-
- increment() {
- this.state.counter++;
- }
-}
-
-//------------------------------------------------------------------------------
-// Message Widget
-//------------------------------------------------------------------------------
-class Message extends owl.Component {
- widgets = { Counter };
- template = "Message";
-
- shouldUpdate(nextProps) {
- return nextProps !== this.props;
- }
- removeMessage() {
- this.trigger("remove_message", {
- id: this.props.id
- });
- }
-}
-
-//------------------------------------------------------------------------------
-// Root Widget
-//------------------------------------------------------------------------------
-class App extends owl.Component {
- widgets = { Message };
- state = { messages: [], multipleFlag: false, clearAfterFlag: false };
- template = "App";
-
- mounted() {
- this.log(
- `Benchmarking Owl v${owl._version} (build date: ${
- owl._date
- })`
- );
- }
-
- benchmark(message, fn, callback) {
- if (this.state.multipleFlag) {
- const N = 20;
- let n = N;
- let total = 0;
- let cb = info => {
- let finalize = () => {
- n--;
- total += info.delta;
- if (n === 0) {
- const avg = total / N;
- this.log(`Average: ${formatNumber(avg)}ms`, true);
- if (callback) {
- callback();
- }
- } else {
- this._benchmark(message, fn, cb);
- }
- };
-
- if (this.state.clearAfterFlag) {
- this._benchmark(
- "clear",
- () => {
- this.state.messages = [];
- },
- finalize,
- false
- );
- } else {
- finalize();
- }
- };
- this._benchmark(message, fn, cb);
- } else {
- this._benchmark(message, fn, callback);
- }
- }
-
- _benchmark(message, fn, cb, log = true) {
- setTimeout(() => {
- startMeasure(message);
- fn();
- stopMeasure(info => {
- if (log) {
- this.log(info.msg);
- }
- if (cb) {
- cb(info);
- }
- });
- }, 10);
- }
-
- addMessages(n) {
- this.benchmark("add " + n, () => {
- const newMessages = buildData(n);
- this.state.messages.push.apply(this.state.messages, newMessages);
- });
- }
-
- clear() {
- this._benchmark("clear", () => {
- this.state.messages = [];
- });
- }
-
- updateSomeMessages() {
- this.benchmark("update every 10th", () => {
- const messages = this.state.messages;
- for (let i = 0; i < this.state.messages.length; i += 10) {
- const msg = Object.assign({}, messages[i]);
- msg.author += "!!!";
- this.set(messages, i, msg);
- }
- });
- }
-
- removeMessage(data) {
- this.benchmark("remove message", () => {
- const index = this.state.messages.findIndex(m => m.id === data.id);
- this.state.messages.splice(index, 1);
- });
- }
-
- log(str, isBold) {
- const div = document.createElement("div");
- if (isBold) {
- div.classList.add("bold");
- }
- div.textContent = `> ${str}`;
- this.refs.log.appendChild(div);
- this.refs.log.scrollTop = this.refs.log.scrollHeight;
- }
-
- clearLog() {
- this.refs.log.innerHTML = "";
- }
-
- toggleMultiple() {
- this.state.multipleFlag = !this.state.multipleFlag;
- }
-
- toggleClear() {
- this.state.clearAfterFlag = !this.state.clearAfterFlag;
- }
-}
-
-//------------------------------------------------------------------------------
-// Application initialization
-//------------------------------------------------------------------------------
-async function start() {
- const templates = await owl.utils.loadTemplates("templates.xml");
- const env = {
- qweb: new owl.QWeb(templates)
- };
- const app = new App(env);
- app.mount(document.body);
-}
-
-start();
diff --git a/tools/benchmarks/owl-0.8.0/index.html b/tools/benchmarks/owl-0.8.0/index.html
deleted file mode 100644
index 1e635556..00000000
--- a/tools/benchmarks/owl-0.8.0/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
- OWL 0.8.0 Benchmark
-
-
-
-
-
-
-
-
diff --git a/tools/benchmarks/owl-0.8.0/owl-0.8.0.js b/tools/benchmarks/owl-0.8.0/owl-0.8.0.js
deleted file mode 100644
index 2877ab50..00000000
--- a/tools/benchmarks/owl-0.8.0/owl-0.8.0.js
+++ /dev/null
@@ -1,2414 +0,0 @@
-(function (exports) {
- 'use strict';
-
- /**
- * We define here a simple event bus: it can
- * - emit events
- * - add/remove listeners.
- *
- * This is a useful pattern of communication in some cases.
- */
- //------------------------------------------------------------------------------
- // EventBus
- //------------------------------------------------------------------------------
- class EventBus {
- constructor() {
- this.subscriptions = {};
- }
- /**
- * Add a listener for the 'eventType' events.
- *
- * Note that the 'owner' of this event can be anything, but will more likely
- * be a widget or a class. The idea is that the callback will be called with
- * the proper owner bound.
- *
- * Also, the owner should be kind of unique. This will be used to remove the
- * listener.
- */
- on(eventType, owner, callback) {
- if (!callback) {
- throw new Error("Missing callback");
- }
- if (!this.subscriptions[eventType]) {
- this.subscriptions[eventType] = [];
- }
- this.subscriptions[eventType].push({
- owner,
- callback
- });
- }
- /**
- * Remove a listener
- */
- off(eventType, owner) {
- const subs = this.subscriptions[eventType];
- if (subs) {
- this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
- }
- }
- /**
- * Emit an event of type 'eventType'. Any extra arguments will be passed to
- * the listeners callback.
- */
- trigger(eventType, ...args) {
- const subs = this.subscriptions[eventType] || [];
- for (let sub of subs) {
- sub.callback.call(sub.owner, ...args);
- }
- }
- /**
- * Remove all subscriptions.
- */
- clear() {
- this.subscriptions = {};
- }
- }
-
- //------------------------------------------------------------------------------
- // Observer
- //------------------------------------------------------------------------------
- const methodsToPatch = [
- "push",
- "pop",
- "shift",
- "unshift",
- "splice",
- "sort",
- "reverse"
- ];
- const ArrayProto = Array.prototype;
- const ModifiedArrayProto = Object.create(ArrayProto);
- for (let method of methodsToPatch) {
- const initialMethod = ArrayProto[method];
- ModifiedArrayProto[method] = function (...args) {
- if (!this.__observer__.allowMutations) {
- throw new Error(`Array cannot be changed here")`);
- }
- this.__observer__.notifyChange();
- this.__owl__.rev++;
- let parent = this;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent));
- let inserted;
- switch (method) {
- case "push":
- case "unshift":
- inserted = args;
- break;
- case "splice":
- inserted = args.slice(2);
- break;
- }
- if (inserted) {
- for (let elem of inserted) {
- this.__observer__.observe(elem, this);
- }
- }
- return initialMethod.call(this, ...args);
- };
- }
- class Observer {
- constructor() {
- this.rev = 1;
- this.allowMutations = true;
- this.dirty = false;
- }
- notifyCB() { }
- notifyChange() {
- this.rev++;
- this.dirty = true;
- Promise.resolve().then(() => {
- if (this.dirty) {
- this.dirty = false;
- this.notifyCB();
- }
- });
- }
- observe(value, parent) {
- if (value === null) {
- // fun fact: typeof null === 'object'
- return;
- }
- if (typeof value !== "object") {
- return;
- }
- if ("__owl__" in value) {
- // already observed
- value.__owl__.parent = parent;
- return;
- }
- if (Array.isArray(value)) {
- this._observeArr(value, parent);
- }
- else {
- this._observeObj(value, parent);
- }
- }
- set(target, key, value) {
- this._addProp(target, key, value);
- target.__owl__.rev++;
- this.notifyChange();
- }
- _observeObj(obj, parent) {
- const keys = Object.keys(obj);
- obj.__owl__ = { rev: 1, deepRev: 1, parent };
- Object.defineProperty(obj, "__owl__", { enumerable: false });
- for (let key of keys) {
- this._addProp(obj, key, obj[key]);
- }
- }
- _observeArr(arr, parent) {
- arr.__owl__ = { rev: 1, deepRev: 1, parent };
- Object.defineProperty(arr, "__owl__", { enumerable: false });
- arr.__proto__ = Object.create(ModifiedArrayProto);
- arr.__proto__.__observer__ = this;
- for (let i = 0; i < arr.length; i++) {
- this.observe(arr[i], arr);
- }
- }
- _addProp(obj, key, value) {
- var self = this;
- Object.defineProperty(obj, key, {
- enumerable: true,
- get() {
- return value;
- },
- set(newVal) {
- if (newVal !== value) {
- if (!self.allowMutations) {
- throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`);
- }
- value = newVal;
- self.observe(newVal, obj);
- obj.__owl__.rev++;
- let parent = obj;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent) && parent !== obj);
- self.notifyChange();
- }
- }
- });
- this.observe(value, obj);
- }
- }
-
- function escape(str) {
- if (str === undefined) {
- return "";
- }
- if (typeof str === "number") {
- return String(str);
- }
- return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, "'")
- .replace(/`/g, "`");
- }
- /**
- * Remove trailing and leading spaces
- */
- function htmlTrim(s) {
- let result = s.replace(/(^\s+|\s+$)/g, "");
- if (s[0] === " ") {
- result = " " + result;
- }
- if (result !== " " && s[s.length - 1] === " ") {
- result = result + " ";
- }
- return result;
- }
- /**
- * Create a function that will generate unique id numbers
- */
- function idGenerator() {
- let nextID = 1;
- return () => nextID++;
- }
- function memoize(f, hash) {
- if (!hash) {
- hash = args => args.map(a => String(a)).join(",");
- }
- let cache = {};
- function memoizedFunction(...args) {
- let hashValue = hash(args);
- if (!(hashValue in cache)) {
- cache[hashValue] = f(...args);
- }
- return cache[hashValue];
- }
- return memoizedFunction;
- }
- /**
- * Returns a function, that, as long as it continues to be invoked, will not
- * be triggered. The function will be called after it stops being called for
- * N milliseconds. If `immediate` is passed, trigger the function on the
- * leading edge, instead of the trailing.
- *
- * Inspired by https://davidwalsh.name/javascript-debounce-function
- */
- function debounce(func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- function later() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- }
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
- }
- /**
- * Find a node in a tree.
- *
- * This will traverse the tree (depth first) and return the first child that
- * matches the predicate, if any
- */
- function findInTree(tree, predicate) {
- if (predicate(tree)) {
- return tree;
- }
- for (let child of tree.children) {
- let match = findInTree(child, predicate);
- if (match) {
- return match;
- }
- }
- return null;
- }
- function patch(C, patchName, patch) {
- const proto = C.prototype;
- if (!proto.__patches) {
- proto.__patches = {
- origMethods: {},
- patches: {},
- current: []
- };
- }
- if (proto.__patches.patches[patchName]) {
- throw new Error(`Patch [${patchName}] already exists`);
- }
- proto.__patches.patches[patchName] = patch;
- applyPatch(proto, patch);
- proto.__patches.current.push(patchName);
- function applyPatch(proto, patch) {
- Object.keys(patch).forEach(function (methodName) {
- const method = patch[methodName];
- if (typeof method === "function") {
- const original = proto[methodName];
- if (!(methodName in proto.__patches.origMethods)) {
- proto.__patches.origMethods[methodName] = original;
- }
- proto[methodName] = function (...args) {
- this._super = original;
- return method.call(this, ...args);
- };
- }
- });
- }
- }
- function unpatch(C, patchName) {
- const proto = C.prototype;
- const patchInfo = proto.__patches;
- delete proto.__patches;
- // reset to original
- for (let k in patchInfo.origMethods) {
- proto[k] = patchInfo.origMethods[k];
- }
- // apply other patches
- for (let name of patchInfo.current) {
- if (name !== patchName) {
- patch(C, name, patchInfo.patches[name]);
- }
- }
- }
- async function loadTemplates(url) {
- const result = await fetch(url);
- if (!result.ok) {
- throw new Error("Error while fetching xml templates");
- }
- let templates = await result.text();
- templates = templates.replace(//g, "");
- return templates;
- }
-
- var _utils = /*#__PURE__*/Object.freeze({
- escape: escape,
- htmlTrim: htmlTrim,
- idGenerator: idGenerator,
- memoize: memoize,
- debounce: debounce,
- findInTree: findInTree,
- patch: patch,
- unpatch: unpatch,
- loadTemplates: loadTemplates
- });
-
- function vnode(sel, data, children, text, elm) {
- let key = data === undefined ? undefined : data.key;
- return {
- sel: sel,
- data: data,
- children: children,
- text: text,
- elm: elm,
- key: key
- };
- }
- //------------------------------------------------------------------------------
- // snabbdom.ts
- //------------------------------------------------------------------------------
- function isUndef(s) {
- return s === undefined;
- }
- function isDef(s) {
- return s !== undefined;
- }
- const emptyNode = vnode("", {}, [], undefined, undefined);
- function sameVnode(vnode1, vnode2) {
- return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
- }
- function isVnode(vnode) {
- return vnode.sel !== undefined;
- }
- function createKeyToOldIdx(children, beginIdx, endIdx) {
- let i, map = {}, key, ch;
- for (i = beginIdx; i <= endIdx; ++i) {
- ch = children[i];
- if (ch != null) {
- key = ch.key;
- if (key !== undefined)
- map[key] = i;
- }
- }
- return map;
- }
- const hooks = [
- "create",
- "update",
- "remove",
- "destroy",
- "pre",
- "post"
- ];
- function init(modules, domApi) {
- let i, j, cbs = {};
- const api = domApi !== undefined ? domApi : htmlDomApi;
- for (i = 0; i < hooks.length; ++i) {
- cbs[hooks[i]] = [];
- for (j = 0; j < modules.length; ++j) {
- const hook = modules[j][hooks[i]];
- if (hook !== undefined) {
- cbs[hooks[i]].push(hook);
- }
- }
- }
- function emptyNodeAt(elm) {
- const id = elm.id ? "#" + elm.id : "";
- const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
- return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
- }
- function createRmCb(childElm, listeners) {
- return function rmCb() {
- if (--listeners === 0) {
- const parent = api.parentNode(childElm);
- api.removeChild(parent, childElm);
- }
- };
- }
- function createElm(vnode, insertedVnodeQueue) {
- let i, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.init))) {
- i(vnode);
- data = vnode.data;
- }
- }
- let children = vnode.children, sel = vnode.sel;
- if (sel === "!") {
- if (isUndef(vnode.text)) {
- vnode.text = "";
- }
- vnode.elm = api.createComment(vnode.text);
- }
- else if (sel !== undefined) {
- // Parse selector
- const hashIdx = sel.indexOf("#");
- const dotIdx = sel.indexOf(".", hashIdx);
- const hash = hashIdx > 0 ? hashIdx : sel.length;
- const dot = dotIdx > 0 ? dotIdx : sel.length;
- const tag = hashIdx !== -1 || dotIdx !== -1
- ? sel.slice(0, Math.min(hash, dot))
- : sel;
- const elm = (vnode.elm =
- isDef(data) && isDef((i = data.ns))
- ? api.createElementNS(i, tag)
- : api.createElement(tag));
- if (hash < dot)
- elm.setAttribute("id", sel.slice(hash + 1, dot));
- if (dotIdx > 0)
- elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
- for (i = 0; i < cbs.create.length; ++i)
- cbs.create[i](emptyNode, vnode);
- if (array(children)) {
- for (i = 0; i < children.length; ++i) {
- const ch = children[i];
- if (ch != null) {
- api.appendChild(elm, createElm(ch, insertedVnodeQueue));
- }
- }
- }
- else if (primitive(vnode.text)) {
- api.appendChild(elm, api.createTextNode(vnode.text));
- }
- i = vnode.data.hook; // Reuse variable
- if (isDef(i)) {
- if (i.create)
- i.create(emptyNode, vnode);
- if (i.insert)
- insertedVnodeQueue.push(vnode);
- }
- }
- else {
- vnode.elm = api.createTextNode(vnode.text);
- }
- return vnode.elm;
- }
- function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
- for (; startIdx <= endIdx; ++startIdx) {
- const ch = vnodes[startIdx];
- if (ch != null) {
- api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
- }
- }
- }
- function invokeDestroyHook(vnode) {
- let i, j, data = vnode.data;
- if (data !== undefined) {
- if (isDef((i = data.hook)) && isDef((i = i.destroy)))
- i(vnode);
- for (i = 0; i < cbs.destroy.length; ++i)
- cbs.destroy[i](vnode);
- if (vnode.children !== undefined) {
- for (j = 0; j < vnode.children.length; ++j) {
- i = vnode.children[j];
- if (i != null && typeof i !== "string") {
- invokeDestroyHook(i);
- }
- }
- }
- }
- }
- function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
- for (; startIdx <= endIdx; ++startIdx) {
- let i, listeners, rm, ch = vnodes[startIdx];
- if (ch != null) {
- if (isDef(ch.sel)) {
- invokeDestroyHook(ch);
- listeners = cbs.remove.length + 1;
- rm = createRmCb(ch.elm, listeners);
- for (i = 0; i < cbs.remove.length; ++i)
- cbs.remove[i](ch, rm);
- if (isDef((i = ch.data)) &&
- isDef((i = i.hook)) &&
- isDef((i = i.remove))) {
- i(ch, rm);
- }
- else {
- rm();
- }
- }
- else {
- // Text node
- api.removeChild(parentElm, ch.elm);
- }
- }
- }
- }
- function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
- let oldStartIdx = 0, newStartIdx = 0;
- let oldEndIdx = oldCh.length - 1;
- let oldStartVnode = oldCh[0];
- let oldEndVnode = oldCh[oldEndIdx];
- let newEndIdx = newCh.length - 1;
- let newStartVnode = newCh[0];
- let newEndVnode = newCh[newEndIdx];
- let oldKeyToIdx;
- let idxInOld;
- let elmToMove;
- let before;
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
- if (oldStartVnode == null) {
- oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
- }
- else if (oldEndVnode == null) {
- oldEndVnode = oldCh[--oldEndIdx];
- }
- else if (newStartVnode == null) {
- newStartVnode = newCh[++newStartIdx];
- }
- else if (newEndVnode == null) {
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newStartVnode)) {
- patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
- oldStartVnode = oldCh[++oldStartIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else if (sameVnode(oldEndVnode, newEndVnode)) {
- patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
- oldEndVnode = oldCh[--oldEndIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldStartVnode, newEndVnode)) {
- // Vnode moved right
- patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
- oldStartVnode = oldCh[++oldStartIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (sameVnode(oldEndVnode, newStartVnode)) {
- // Vnode moved left
- patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
- api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode = oldCh[--oldEndIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- if (oldKeyToIdx === undefined) {
- oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
- }
- idxInOld = oldKeyToIdx[newStartVnode.key];
- if (isUndef(idxInOld)) {
- // New element
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- elmToMove = oldCh[idxInOld];
- if (elmToMove.sel !== newStartVnode.sel) {
- api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- }
- else {
- patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
- oldCh[idxInOld] = undefined;
- api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
- }
- newStartVnode = newCh[++newStartIdx];
- }
- }
- }
- if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
- if (oldStartIdx > oldEndIdx) {
- before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
- addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
- }
- else {
- removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
- }
- }
- }
- function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
- let i, hook;
- if (isDef((i = vnode.data)) &&
- isDef((hook = i.hook)) &&
- isDef((i = hook.prepatch))) {
- i(oldVnode, vnode);
- }
- const elm = (vnode.elm = oldVnode.elm);
- let oldCh = oldVnode.children;
- let ch = vnode.children;
- if (oldVnode === vnode)
- return;
- if (vnode.data !== undefined) {
- for (i = 0; i < cbs.update.length; ++i)
- cbs.update[i](oldVnode, vnode);
- i = vnode.data.hook;
- if (isDef(i) && isDef((i = i.update)))
- i(oldVnode, vnode);
- }
- if (isUndef(vnode.text)) {
- if (isDef(oldCh) && isDef(ch)) {
- if (oldCh !== ch)
- updateChildren(elm, oldCh, ch, insertedVnodeQueue);
- }
- else if (isDef(ch)) {
- if (isDef(oldVnode.text))
- api.setTextContent(elm, "");
- addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
- }
- else if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- else if (isDef(oldVnode.text)) {
- api.setTextContent(elm, "");
- }
- }
- else if (oldVnode.text !== vnode.text) {
- if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- }
- api.setTextContent(elm, vnode.text);
- }
- if (isDef(hook) && isDef((i = hook.postpatch))) {
- i(oldVnode, vnode);
- }
- }
- return function patch(oldVnode, vnode) {
- let i, elm, parent;
- const insertedVnodeQueue = [];
- for (i = 0; i < cbs.pre.length; ++i)
- cbs.pre[i]();
- if (!isVnode(oldVnode)) {
- oldVnode = emptyNodeAt(oldVnode);
- }
- if (sameVnode(oldVnode, vnode)) {
- patchVnode(oldVnode, vnode, insertedVnodeQueue);
- }
- else {
- elm = oldVnode.elm;
- parent = api.parentNode(elm);
- createElm(vnode, insertedVnodeQueue);
- if (parent !== null) {
- api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
- removeVnodes(parent, [oldVnode], 0, 0);
- }
- }
- for (i = 0; i < insertedVnodeQueue.length; ++i) {
- insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
- }
- for (i = 0; i < cbs.post.length; ++i)
- cbs.post[i]();
- return vnode;
- };
- }
- //------------------------------------------------------------------------------
- // is.ts
- //------------------------------------------------------------------------------
- const array = Array.isArray;
- function primitive(s) {
- return typeof s === "string" || typeof s === "number";
- }
- function createElement(tagName) {
- return document.createElement(tagName);
- }
- function createElementNS(namespaceURI, qualifiedName) {
- return document.createElementNS(namespaceURI, qualifiedName);
- }
- function createTextNode(text) {
- return document.createTextNode(text);
- }
- function createComment(text) {
- return document.createComment(text);
- }
- function insertBefore(parentNode, newNode, referenceNode) {
- parentNode.insertBefore(newNode, referenceNode);
- }
- function removeChild(node, child) {
- node.removeChild(child);
- }
- function appendChild(node, child) {
- node.appendChild(child);
- }
- function parentNode(node) {
- return node.parentNode;
- }
- function nextSibling(node) {
- return node.nextSibling;
- }
- function tagName(elm) {
- return elm.tagName;
- }
- function setTextContent(node, text) {
- node.textContent = text;
- }
- function getTextContent(node) {
- return node.textContent;
- }
- function isElement(node) {
- return node.nodeType === 1;
- }
- function isText(node) {
- return node.nodeType === 3;
- }
- function isComment(node) {
- return node.nodeType === 8;
- }
- const htmlDomApi = {
- createElement,
- createElementNS,
- createTextNode,
- createComment,
- insertBefore,
- removeChild,
- appendChild,
- parentNode,
- nextSibling,
- tagName,
- setTextContent,
- getTextContent,
- isElement,
- isText,
- isComment
- };
- function addNS(data, children, sel) {
- data.ns = "http://www.w3.org/2000/svg";
- if (sel !== "foreignObject" && children !== undefined) {
- for (let i = 0; i < children.length; ++i) {
- let childData = children[i].data;
- if (childData !== undefined) {
- addNS(childData, children[i].children, children[i].sel);
- }
- }
- }
- }
- function h(sel, b, c) {
- var data = {}, children, text, i;
- if (c !== undefined) {
- data = b;
- if (array(c)) {
- children = c;
- }
- else if (primitive(c)) {
- text = c;
- }
- else if (c && c.sel) {
- children = [c];
- }
- }
- else if (b !== undefined) {
- if (array(b)) {
- children = b;
- }
- else if (primitive(b)) {
- text = b;
- }
- else if (b && b.sel) {
- children = [b];
- }
- else {
- data = b;
- }
- }
- if (children !== undefined) {
- for (i = 0; i < children.length; ++i) {
- if (primitive(children[i]))
- children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
- }
- }
- if (sel[0] === "s" &&
- sel[1] === "v" &&
- sel[2] === "g" &&
- (sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
- addNS(data, children, sel);
- }
- return vnode(sel, data, children, text, undefined);
- }
- function updateProps(oldVnode, vnode) {
- var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
- if (!oldProps && !props)
- return;
- if (oldProps === props)
- return;
- oldProps = oldProps || {};
- props = props || {};
- for (key in oldProps) {
- if (!props[key]) {
- delete elm[key];
- }
- }
- for (key in props) {
- cur = props[key];
- old = oldProps[key];
- if (old !== cur && (key !== "value" || elm[key] !== cur)) {
- elm[key] = cur;
- }
- }
- }
- const propsModule = {
- create: updateProps,
- update: updateProps
- };
- function invokeHandler(handler, vnode, event) {
- if (typeof handler === "function") {
- // call function handler
- handler.call(vnode, event, vnode);
- }
- else if (typeof handler === "object") {
- // call handler with arguments
- if (typeof handler[0] === "function") {
- // special case for single argument for performance
- if (handler.length === 2) {
- handler[0].call(vnode, handler[1], event, vnode);
- }
- else {
- var args = handler.slice(1);
- args.push(event);
- args.push(vnode);
- handler[0].apply(vnode, args);
- }
- }
- else {
- // call multiple handlers
- for (var i = 0; i < handler.length; i++) {
- invokeHandler(handler[i], vnode, event);
- }
- }
- }
- }
- function handleEvent(event, vnode) {
- var name = event.type, on = vnode.data.on;
- // call event handler(s) if exists
- if (on && on[name]) {
- invokeHandler(on[name], vnode, event);
- }
- }
- function createListener() {
- return function handler(event) {
- handleEvent(event, handler.vnode);
- };
- }
- function updateEventListeners(oldVnode, vnode) {
- var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name;
- // optimization for reused immutable handlers
- if (oldOn === on) {
- return;
- }
- // remove existing listeners which no longer used
- if (oldOn && oldListener) {
- // if element changed or deleted we remove all existing listeners unconditionally
- if (!on) {
- for (name in oldOn) {
- // remove listener if element was changed or existing listeners removed
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- else {
- for (name in oldOn) {
- // remove listener if existing listener removed
- if (!on[name]) {
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- }
- }
- // add new listeners which has not already attached
- if (on) {
- // reuse existing listener or create new
- var listener = (vnode.listener =
- oldVnode.listener || createListener());
- // update vnode for listener
- listener.vnode = vnode;
- // if element changed or added we add all needed listeners unconditionally
- if (!oldOn) {
- for (name in on) {
- // add listener if element was changed or new listeners added
- elm.addEventListener(name, listener, false);
- }
- }
- else {
- for (name in on) {
- // add listener if new listener added
- if (!oldOn[name]) {
- elm.addEventListener(name, listener, false);
- }
- }
- }
- }
- }
- const eventListenersModule = {
- create: updateEventListeners,
- update: updateEventListeners,
- destroy: updateEventListeners
- };
- const xlinkNS = "http://www.w3.org/1999/xlink";
- const xmlNS = "http://www.w3.org/XML/1998/namespace";
- const colonChar = 58;
- const xChar = 120;
- function updateAttrs(oldVnode, vnode) {
- var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs;
- if (!oldAttrs && !attrs)
- return;
- if (oldAttrs === attrs)
- return;
- oldAttrs = oldAttrs || {};
- attrs = attrs || {};
- // update modified attributes, add new attributes
- for (key in attrs) {
- const cur = attrs[key];
- const old = oldAttrs[key];
- if (old !== cur) {
- if (cur === true) {
- elm.setAttribute(key, "");
- }
- else if (cur === false) {
- elm.removeAttribute(key);
- }
- else {
- if (key.charCodeAt(0) !== xChar) {
- elm.setAttribute(key, cur);
- }
- else if (key.charCodeAt(3) === colonChar) {
- // Assume xml namespace
- elm.setAttributeNS(xmlNS, key, cur);
- }
- else if (key.charCodeAt(5) === colonChar) {
- // Assume xlink namespace
- elm.setAttributeNS(xlinkNS, key, cur);
- }
- else {
- elm.setAttribute(key, cur);
- }
- }
- }
- }
- // remove removed attributes
- // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
- // the other option is to remove all attributes with value == undefined
- for (key in oldAttrs) {
- if (!(key in attrs)) {
- elm.removeAttribute(key);
- }
- }
- }
- const attrsModule = {
- create: updateAttrs,
- update: updateAttrs
- };
- const patch$1 = init([eventListenersModule, attrsModule, propsModule]);
-
- let getId = idGenerator();
- //------------------------------------------------------------------------------
- // Widget
- //------------------------------------------------------------------------------
- class Component extends EventBus {
- //--------------------------------------------------------------------------
- // Lifecycle
- //--------------------------------------------------------------------------
- /**
- * Creates an instance of Component.
- *
- * The root widget of a component tree needs an environment:
- *
- * ```javascript
- * const root = new RootWidget(env, props);
- * ```
- *
- * Every other widget simply needs a reference to its parent:
- *
- * ```javascript
- * const child = new SomeWidget(parent, props);
- * ```
- *
- * Note that most of the time, only the root widget needs to be created by
- * hand. Other widgets should be created automatically by the framework (with
- * the t-widget directive in a template)
- */
- constructor(parent, props) {
- super();
- this.template = "default";
- this.inlineTemplate = null;
- this.refs = {};
- // is this a good idea?
- // Pro: if props is empty, we can create easily a widget
- // Con: this is not really safe
- // Pro: but creating widget (by a template) is always unsafe anyway
- this.props = props || {};
- let id = getId();
- let p = null;
- if (parent instanceof Component) {
- p = parent;
- this.env = parent.env;
- parent.__owl__.children[id] = this;
- }
- else {
- this.env = parent;
- }
- this.__owl__ = {
- id: id,
- vnode: null,
- isStarted: false,
- isMounted: false,
- isDestroyed: false,
- parent: p,
- children: {},
- cmap: {},
- renderId: 1,
- renderPromise: null,
- renderProps: props || null,
- boundHandlers: {}
- };
- }
- get el() {
- return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
- }
- /**
- * willStart is an asynchronous hook that can be implemented to perform some
- * action before the initial rendering of a component.
- *
- * It will be called exactly once before the initial rendering. It is useful
- * in some cases, for example, to load external assets (such as a JS library)
- * before the widget is rendered.
- *
- * Note that a slow willStart method will slow down the rendering of the user
- * interface. Therefore, some effort should be made to make this method as
- * fast as possible.
- *
- * Note: this method should not be called manually.
- */
- async willStart() { }
- /**
- * mounted is a hook that is called each time a component is attached to the
- * DOM. This is a good place to add some listeners, or to interact with the
- * DOM, if the component needs to perform some measure for example.
- *
- * Note: this method should not be called manually.
- *
- * @see willUnmount
- */
- mounted() { }
- /**
- * The willUpdateProps is an asynchronous hook, called just before new props
- * are set. This is useful if the component needs some asynchronous task
- * performed, depending on the props (for example, assuming that the props are
- * some record Id, fetching the record data).
- *
- * This hook is not called during the first render (but willStart is called
- * and performs a similar job).
- */
- async willUpdateProps(nextProps) { }
- /**
- * The willPatch hook is called just before the DOM patching process starts.
- * It is not called on the initial render. This is useful to get some
- * information which are in the DOM. For example, the current position of the
- * scrollbar
- */
- willPatch() { }
- /**
- * This hook is called whenever a component did actually update its props,
- * state or env.
- *
- * This method is not called on the initial render. It is useful to interact
- * with the DOM (for example, through an external library) whenever the
- * component was updated.
- *
- * Updating the widget state in this hook is possible, but not encouraged.
- * One need to be careful, because updates here will cause rerender, which in
- * turn will cause other calls to updated. So, we need to be particularly
- * careful at avoiding endless cycles.
- */
- patched() { }
- /**
- * willUnmount is a hook that is called each time just before a component is
- * unmounted from the DOM. This is a good place to remove some listeners, for
- * example.
- *
- * Note: this method should not be called manually.
- *
- * @see mounted
- */
- willUnmount() { }
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
- /**
- * Attach a child widget to a given html element
- *
- * This is most of the time not necessary, since widgets should primarily be
- * created/managed with the t-widget directive in a qweb template. However,
- * for the cases where we need more control, this method will do what is
- * necessary to make sure all the proper hooks are called (for example,
- * mounted/willUnmount)
- *
- * Note that this method makes a few assumptions:
- * - the child widget is indeed a child of the current widget
- * - the target is inside the dom of the current widget (typically a ref)
- */
- attachChild(child, target) {
- target.appendChild(child.el);
- child.__mount();
- }
- async mount(target) {
- const vnode = await this._prepare();
- if (this.__owl__.isDestroyed) {
- // widget was destroyed before we get here...
- return;
- }
- this._patch(vnode);
- target.appendChild(this.el);
- if (document.body.contains(target)) {
- this._visitSubTree(w => {
- if (!w.__owl__.isMounted && this.el.contains(w.el)) {
- w.__owl__.isMounted = true;
- w.mounted();
- return true;
- }
- return false;
- });
- }
- }
- unmount() {
- if (this.el) {
- this._visitSubTree(w => {
- if (w.__owl__.isMounted) {
- w.willUnmount();
- w.__owl__.isMounted = false;
- return true;
- }
- return false;
- });
- this.el.remove();
- }
- }
- async render(force = false) {
- if (this.__owl__.isDestroyed) {
- return;
- }
- const renderVDom = this._render(force);
- const renderId = this.__owl__.renderId;
- const vnode = await renderVDom;
- if (renderId === this.__owl__.renderId) {
- // we only update the vnode and the actual DOM if no other rendering
- // occurred between now and when the render method was initially called.
- this._patch(vnode);
- }
- }
- destroy() {
- if (!this.__owl__.isDestroyed) {
- for (let id in this.__owl__.children) {
- this.__owl__.children[id].destroy();
- }
- if (this.__owl__.isMounted) {
- this.willUnmount();
- }
- if (this.el) {
- this.el.remove();
- this.__owl__.isMounted = false;
- delete this.__owl__.vnode;
- }
- if (this.__owl__.parent) {
- let id = this.__owl__.id;
- delete this.__owl__.parent.__owl__.children[id];
- this.__owl__.parent = null;
- }
- this.clear();
- this.__owl__.isDestroyed = true;
- }
- }
- shouldUpdate(nextProps) {
- return true;
- }
- /**
- * This method is the correct way to update the environment of a widget. Doing
- * this will cause a full rerender of the widget and its children, so this is
- * an operation that should not be done frequently.
- *
- * A good usecase for updating the environment would be to update some mostly
- * static config keys, such as a boolean to determine if we are in mobile
- * mode or not.
- */
- async updateEnv(nextEnv) {
- if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
- this.env = Object.create(this.env);
- }
- Object.assign(this.env, nextEnv);
- if (this.__owl__.isMounted) {
- await this.render(true);
- }
- }
- async updateProps(nextProps, forceUpdate = false) {
- const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
- return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
- }
- set(target, key, value) {
- this.__owl__.observer.set(target, key, value);
- }
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- async _updateProps(nextProps) {
- await this.willUpdateProps(nextProps);
- this.props = nextProps;
- await this.render();
- }
- _patch(vnode) {
- this.__owl__.renderPromise = null;
- if (this.__owl__.vnode) {
- this.willPatch();
- this.__owl__.vnode = patch$1(this.__owl__.vnode, vnode);
- this.patched();
- }
- else {
- this.__owl__.vnode = patch$1(document.createElement(vnode.sel), vnode);
- }
- }
- async _prepare() {
- this.__owl__.renderProps = this.props;
- this.__owl__.renderPromise = this.willStart().then(() => {
- if (this.__owl__.isDestroyed) {
- return Promise.resolve(h("div"));
- }
- this.__owl__.isStarted = true;
- if (this.inlineTemplate) {
- this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
- }
- this._observeState();
- return this._render();
- });
- return this.__owl__.renderPromise;
- }
- async _render(force = false) {
- this.__owl__.renderId++;
- const promises = [];
- const template = this.inlineTemplate || this.template;
- if (this.__owl__.observer) {
- this.__owl__.observer.allowMutations = false;
- }
- let vnode = this.env.qweb.render(template, this, {
- promises,
- handlers: this.__owl__.boundHandlers,
- forceUpdate: force
- });
- if (this.__owl__.observer) {
- this.__owl__.observer.allowMutations = true;
- }
- // this part is critical for the patching process to be done correctly. The
- // tricky part is that a child widget can be rerendered on its own, which
- // will update its own vnode representation without the knowledge of the
- // parent widget. With this, we make sure that the parent widget will be
- // able to patch itself properly after
- vnode.key = this.__owl__.id;
- this.__owl__.renderProps = this.props;
- this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
- return this.__owl__.renderPromise;
- }
- /**
- * Only called by qweb t-widget directive
- */
- _mount(vnode, elm) {
- this.__owl__.vnode = patch$1(elm, vnode);
- this.__mount();
- return this.__owl__.vnode;
- }
- __mount() {
- if (this.__owl__.isMounted) {
- return;
- }
- this._observeState();
- if (this.__owl__.parent) {
- if (this.__owl__.parent.__owl__.isMounted) {
- this.__owl__.isMounted = true;
- this.mounted();
- const children = this.__owl__.children;
- for (let id in children) {
- children[id].__mount();
- }
- }
- }
- }
- _visitSubTree(callback) {
- const shouldVisitChildren = callback(this);
- if (shouldVisitChildren) {
- const children = this.__owl__.children;
- for (let id in children) {
- children[id]._visitSubTree(callback);
- }
- }
- }
- _observeState() {
- if (this.state) {
- this.__owl__.observer = new Observer();
- this.__owl__.observer.observe(this.state);
- this.__owl__.observer.notifyCB = this.render.bind(this);
- }
- }
- }
-
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const DISABLED_TAGS = [
- "input",
- "textarea",
- "button",
- "select",
- "option",
- "optgroup"
- ];
- const lineBreakRE = /[\r\n]/;
- const whitespaceRE = /\s+/g;
- //------------------------------------------------------------------------------
- // Compilation Context
- //------------------------------------------------------------------------------
- class Context {
- constructor(name) {
- this.nextID = 1;
- this.code = [];
- this.variables = {};
- this.definedVariables = {};
- this.escaping = false;
- this.parentNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldProtectContext = false;
- this.inLoop = false;
- this.inPreTag = false;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.addLine("var h = this.utils.h;");
- }
- generateID() {
- const id = this.rootContext.nextID++;
- return id;
- }
- withParent(node) {
- if (this === this.rootContext && this.parentNode) {
- throw new Error("A template should not have more than one root node");
- }
- if (!this.rootContext.rootNode) {
- this.rootContext.rootNode = node;
- }
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- formatExpression(e) {
- e = e.trim();
- if (e[0] === "{" && e[e.length - 1] === "}") {
- const innerExpr = e
- .slice(1, -1)
- .split(",")
- .map(p => {
- let [key, val] = p.trim().split(":");
- if (key === "") {
- return "";
- }
- if (!val) {
- val = key;
- }
- return `${key}: ${this.formatExpression(val)}`;
- })
- .join(",");
- return "{" + innerExpr + "}";
- }
- // Thanks CHM for this code...
- const chars = e.split("");
- let instring = "";
- let invar = "";
- let invarPos = 0;
- let r = "";
- chars.push(" ");
- for (var i = 0, ilen = chars.length; i < ilen; i++) {
- var c = chars[i];
- if (instring.length) {
- if (c === instring && chars[i - 1] !== "\\") {
- instring = "";
- }
- }
- else if (c === '"' || c === "'") {
- instring = c;
- }
- else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
- invar = c;
- invarPos = i;
- continue;
- }
- else if (c.match(/\W/) && invar.length) {
- // TODO: Should check for possible spaces before dot
- if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
- if (!(invar in this.definedVariables)) {
- invar =
- WORD_REPLACEMENT[invar] ||
- (invar in this.variables &&
- this.formatExpression(this.variables[invar])) ||
- "context['" + invar + "']";
- }
- }
- r += invar;
- invar = "";
- }
- else if (invar.length) {
- invar += c;
- continue;
- }
- r += c;
- }
- const result = r.slice(0, -1);
- return result;
- }
- }
- //------------------------------------------------------------------------------
- // QWeb rendering engine
- //------------------------------------------------------------------------------
- class QWeb {
- constructor(data) {
- this.processedTemplates = {};
- this.templates = {};
- this.directives = [];
- this.utils = {
- h: h,
- getFragment(str) {
- const temp = document.createElement("template");
- temp.innerHTML = str;
- return temp.content;
- },
- objectToAttrString(obj) {
- let classes = [];
- for (let k in obj) {
- if (obj[k]) {
- classes.push(k);
- }
- }
- return classes.join(" ");
- }
- };
- this.directiveNames = {
- as: 1,
- name: 1,
- value: 1,
- att: 1,
- attf: 1,
- props: 1,
- key: 1,
- keepalive: 1,
- debug: 1
- };
- [
- forEachDirective,
- escDirective,
- rawDirective,
- setDirective,
- elseDirective,
- elifDirective,
- ifDirective,
- callDirective,
- onDirective,
- refDirective,
- widgetDirective
- ].forEach(d => this.addDirective(d));
- if (data) {
- this.loadTemplates(data);
- }
- }
- addDirective(dir) {
- this.directives.push(dir);
- this.directiveNames[dir.name] = 1;
- this.directives.sort((d1, d2) => d1.priority - d2.priority);
- }
- /**
- * Add a template to the internal template map. Note that it is not
- * immediately compiled.
- */
- addTemplate(name, template, allowDuplicates = false) {
- if (name in this.processedTemplates) {
- if (allowDuplicates) {
- return;
- }
- else {
- throw new Error(`Template ${name} already defined`);
- }
- }
- const parser = new DOMParser();
- const doc = parser.parseFromString(template, "text/xml");
- if (!doc.firstChild) {
- throw new Error("Invalid template (should not be empty)");
- }
- if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
- }
- let elem = doc.firstChild;
- this._processTemplate(elem);
- this.processedTemplates[name] = elem;
- }
- _processTemplate(elem) {
- let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
- let node = tbranch[i];
- let prevElem = node.previousElementSibling;
- let pattr = function (name) {
- return prevElem.getAttribute(name);
- };
- let nattr = function (name) {
- return +!!node.getAttribute(name);
- };
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
- if (pattr("t-foreach")) {
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
- }
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
- return a + b;
- }) > 1) {
- throw new Error("Only one conditional branching directive is allowed per node");
- }
- // All text nodes between branch nodes are removed
- let textNode;
- while ((textNode = node.previousSibling) !== prevElem) {
- if (textNode.nodeValue.trim().length) {
- throw new Error("text is not allowed between branching directives");
- }
- textNode.remove();
- }
- }
- else {
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
- }
- }
- }
- /**
- * Load templates from a xml (as a string). This will look up for the first
- * tag, and will consider each child of this as a template, with
- * the name given by the t-name attribute.
- */
- loadTemplates(xmlstr) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xmlstr, "text/xml");
- if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
- }
- const templates = doc.getElementsByTagName("templates")[0];
- if (!templates) {
- return;
- }
- for (let elem of templates.children) {
- const name = elem.getAttribute("t-name");
- this._processTemplate(elem);
- this.processedTemplates[name] = elem;
- }
- }
- /**
- * Render a template
- *
- * @param {string} name the template should already have been added
- */
- render(name, context = {}, extra = null) {
- if (!(name in this.processedTemplates)) {
- throw new Error(`Template ${name} does not exist`);
- }
- const template = this.templates[name] || this._compile(name);
- return template.call(this, context, extra);
- }
- _compile(name) {
- if (name in this.templates) {
- return this.templates[name];
- }
- const mainNode = this.processedTemplates[name];
- const isDebug = mainNode.attributes.hasOwnProperty("t-debug");
- const ctx = new Context(name);
- this._compileNode(mainNode, ctx);
- if (ctx.shouldProtectContext) {
- ctx.code.unshift(" context = Object.create(context);");
- }
- if (ctx.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- ctx.code.unshift(" let owner = context;");
- }
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
- }
- ctx.addLine(`return vn${ctx.rootNode};`);
- if (isDebug) {
- ctx.code.unshift(" debugger");
- }
- let template;
- try {
- template = new Function("context", "extra", ctx.code.join("\n"));
- }
- catch (e) {
- throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`);
- }
- if (isDebug) {
- console.log(`Template: ${this.processedTemplates[name].outerHTML}\nCompiled code:\n` + template.toString());
- }
- this.templates[name] = template;
- return template;
- }
- /**
- * Generate code from an xml node
- *
- */
- _compileNode(node, ctx) {
- if (!(node instanceof Element)) {
- // this is a text node, there are no directive to apply
- let text = node.textContent;
- if (!ctx.inPreTag) {
- if (lineBreakRE.test(text) && !text.trim()) {
- return;
- }
- text = text.replace(whitespaceRE, " ");
- }
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
- }
- else {
- // this is an unusual situation: this text node is the result of the
- // template rendering.
- let nodeID = ctx.generateID();
- ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
- ctx.rootContext.rootNode = nodeID;
- ctx.rootContext.parentNode = nodeID;
- }
- return;
- }
- const attributes = node.attributes;
- const validDirectives = [];
- let withHandlers = false;
- // maybe this is not optimal: we iterate on all attributes here, and again
- // just after for each directive.
- for (let i = 0; i < attributes.length; i++) {
- let attrName = attributes[i].name;
- if (attrName.startsWith("t-")) {
- let dName = attrName.slice(2).split("-")[0];
- if (!(dName in this.directiveNames)) {
- throw new Error(`Unknown QWeb directive: '${attrName}'`);
- }
- }
- }
- for (let directive of this.directives) {
- let fullName;
- let value;
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- if (name === "t-" + directive.name ||
- name.startsWith("t-" + directive.name + "-")) {
- fullName = name;
- value = attributes[i].textContent;
- validDirectives.push({ directive, value, fullName });
- if (directive.name === "on") {
- withHandlers = true;
- }
- }
- }
- }
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeEncounter) {
- const isDone = directive.atNodeEncounter({
- node,
- qweb: this,
- ctx,
- fullName,
- value
- });
- if (isDone) {
- return;
- }
- }
- }
- if (node.nodeName !== "t") {
- let nodeID = this._compileGenericNode(node, ctx, withHandlers);
- ctx = ctx.withParent(nodeID);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeCreation) {
- directive.atNodeCreation({
- node,
- qweb: this,
- ctx,
- fullName,
- value,
- nodeID
- });
- }
- }
- }
- if (node.nodeName === "pre") {
- ctx = ctx.subContext("inPreTag", true);
- }
- this._compileChildren(node, ctx);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.finalize) {
- directive.finalize({ node, qweb: this, ctx, fullName, value });
- }
- }
- }
- _compileGenericNode(node, ctx, withHandlers = true) {
- // nodeType 1 is generic tag
- if (node.nodeType !== 1) {
- throw new Error("unsupported node type");
- }
- const attributes = node.attributes;
- const attrs = [];
- const props = [];
- const tattrs = [];
- function handleBooleanProps(key, val) {
- let isProp = false;
- if (node.nodeName === "input" && key === "checked") {
- let type = node.getAttribute("type");
- if (type === "checkbox" || type === "radio") {
- isProp = true;
- }
- }
- if (node.nodeName === "option" && key === "selected") {
- isProp = true;
- }
- if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
- isProp = true;
- }
- if ((key === "readonly" && node.nodeName === "input") ||
- node.nodeName === "textarea") {
- isProp = true;
- }
- if (isProp) {
- props.push(`${key}: _${val}`);
- }
- }
- for (let i = 0; i < attributes.length; i++) {
- let name = attributes[i].name;
- const value = attributes[i].textContent;
- // regular attributes
- if (!name.startsWith("t-") &&
- !node.getAttribute("t-attf-" + name)) {
- const attID = ctx.generateID();
- ctx.addLine(`var _${attID} = '${value}';`);
- if (!name.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- name = '"' + name + '"';
- }
- attrs.push(`${name}: _${attID}`);
- handleBooleanProps(name, attID);
- }
- // dynamic attributes
- if (name.startsWith("t-att-")) {
- let attName = name.slice(6);
- let formattedValue = ctx.formatExpression(ctx.getValue(value));
- if (formattedValue[0] === "{" &&
- formattedValue[formattedValue.length - 1] === "}") {
- formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
- }
- const attID = ctx.generateID();
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- // we need to combine dynamic with non dynamic attributes:
- // class="a" t-att-class="'yop'" should be rendered as class="a yop"
- const attValue = node.getAttribute(attName);
- if (attValue) {
- const attValueID = ctx.generateID();
- ctx.addLine(`var _${attValueID} = ${formattedValue};`);
- formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
- const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
- attrs.splice(attrIndex, 1);
- }
- ctx.addLine(`var _${attID} = ${formattedValue};`);
- attrs.push(`${attName}: _${attID}`);
- handleBooleanProps(attName, attID);
- }
- if (name.startsWith("t-attf-")) {
- let attName = name.slice(7);
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- const formattedExpr = value.replace(/\{\{.*?\}\}/g, s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}");
- const attID = ctx.generateID();
- let staticVal = node.getAttribute(attName);
- if (staticVal) {
- ctx.addLine(`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`);
- }
- else {
- ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
- }
- attrs.push(`${attName}: _${attID}`);
- }
- // t-att= attributes
- if (name === "t-att") {
- let id = ctx.generateID();
- ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`);
- tattrs.push(id);
- }
- }
- let nodeID = ctx.generateID();
- let nodeKey = node.getAttribute("t-key");
- if (nodeKey) {
- nodeKey = ctx.formatExpression(nodeKey);
- }
- else {
- nodeKey = nodeID;
- }
- const parts = [`key:${nodeKey}`];
- if (attrs.length + tattrs.length > 0) {
- parts.push(`attrs:{${attrs.join(",")}}`);
- }
- if (props.length > 0) {
- parts.push(`props:{${props.join(",")}}`);
- }
- if (withHandlers) {
- parts.push(`on:{}`);
- }
- ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
- for (let id of tattrs) {
- ctx.addIf(`_${id} instanceof Array`);
- ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
- ctx.addElse();
- ctx.addLine(`for (let key in _${id}) {`);
- ctx.indent();
- ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
- ctx.dedent();
- ctx.addLine(`}`);
- ctx.closeIf();
- }
- ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
- }
- return nodeID;
- }
- _compileChildren(node, ctx) {
- if (node.childNodes.length > 0) {
- for (let child of Array.from(node.childNodes)) {
- this._compileNode(child, ctx);
- }
- }
- }
- }
- function compileValueNode(value, node, qweb, ctx) {
- if (value === "0" && ctx.caller) {
- qweb._compileNode(ctx.caller, ctx);
- return;
- }
- if (typeof value === "string") {
- let exprID = value;
- if (!(value in ctx.definedVariables)) {
- exprID = `_${ctx.generateID()}`;
- ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
- }
- ctx.addIf(`${exprID} || ${exprID} === 0`);
- if (!ctx.parentNode) {
- throw new Error("Should not have a text node without a parent");
- }
- if (ctx.escaping) {
- ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
- }
- else {
- let fragID = ctx.generateID();
- ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
- let tempNodeID = ctx.generateID();
- ctx.addLine(`var p${tempNodeID} = {hook: {`);
- ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
- ctx.addLine(`}};`);
- ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
- ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
- }
- if (node.childNodes.length) {
- ctx.addElse();
- qweb._compileChildren(node, ctx);
- }
- ctx.closeIf();
- return;
- }
- if (value instanceof NodeList) {
- for (let node of Array.from(value)) {
- qweb._compileNode(node, ctx);
- }
- }
- }
- const escDirective = {
- name: "esc",
- priority: 70,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-esc"));
- compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
- return true;
- }
- };
- const rawDirective = {
- name: "raw",
- priority: 80,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
- let value = ctx.getValue(node.getAttribute("t-raw"));
- compileValueNode(value, node, qweb, ctx);
- return true;
- }
- };
- const setDirective = {
- name: "set",
- priority: 60,
- atNodeEncounter({ node, ctx }) {
- const variable = node.getAttribute("t-set");
- let value = node.getAttribute("t-value");
- if (value) {
- const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable)) {
- ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`);
- }
- else {
- const varName = `_${ctx.generateID()}`;
- ctx.addLine(`var ${varName} = ${formattedValue};`);
- ctx.definedVariables[varName] = formattedValue;
- ctx.variables[variable] = varName;
- }
- }
- else {
- ctx.variables[variable] = node.childNodes;
- }
- return true;
- }
- };
- const ifDirective = {
- name: "if",
- priority: 20,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-if"));
- ctx.addIf(`${ctx.formatExpression(cond)}`);
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- };
- const elifDirective = {
- name: "elif",
- priority: 30,
- atNodeEncounter({ node, ctx }) {
- let cond = ctx.getValue(node.getAttribute("t-elif"));
- ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- };
- const elseDirective = {
- name: "else",
- priority: 40,
- atNodeEncounter({ ctx }) {
- ctx.addLine(`else {`);
- ctx.indent();
- return false;
- },
- finalize({ ctx }) {
- ctx.closeIf();
- }
- };
- const callDirective = {
- name: "call",
- priority: 50,
- atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- throw new Error("Invalid tag for t-call directive (should be 't')");
- }
- const subTemplate = node.getAttribute("t-call");
- const nodeTemplate = qweb.processedTemplates[subTemplate];
- if (!nodeTemplate) {
- throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
- }
- const nodeCopy = node.cloneNode(true);
- nodeCopy.removeAttribute("t-call");
- // extract variables from nodecopy
- const tempCtx = new Context();
- tempCtx.nextID = ctx.rootContext.nextID;
- qweb._compileNode(nodeCopy, tempCtx);
- const vars = Object.assign({}, ctx.variables, tempCtx.variables);
- var definedVariables = Object.assign({}, ctx.definedVariables, tempCtx.definedVariables);
- ctx.rootContext.nextID = tempCtx.nextID;
- // open new scope, if necessary
- const hasNewVariables = Object.keys(definedVariables).length > 0;
- if (hasNewVariables) {
- ctx.addLine("{");
- ctx.indent();
- }
- // add new variables, if any
- for (let key in definedVariables) {
- ctx.addLine(`let ${key} = ${definedVariables[key]}`);
- }
- // compile sub template
- const subCtx = ctx
- .subContext("caller", nodeCopy)
- .subContext("variables", Object.create(vars))
- .subContext("definedVariables", Object.create(definedVariables));
- qweb._compileNode(nodeTemplate, subCtx);
- // close new scope
- if (hasNewVariables) {
- ctx.dedent();
- ctx.addLine("}");
- }
- return true;
- }
- };
- const forEachDirective = {
- name: "foreach",
- priority: 10,
- atNodeEncounter({ node, qweb, ctx }) {
- ctx.rootContext.shouldProtectContext = true;
- ctx = ctx.subContext("inLoop", true);
- const elems = node.getAttribute("t-foreach");
- const name = node.getAttribute("t-as");
- let arrayID = ctx.generateID();
- ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
- ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
- ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`);
- let keysID = ctx.generateID();
- ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`);
- let valuesID = ctx.generateID();
- ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`);
- ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
- ctx.indent();
- ctx.addLine(`context.${name}_first = i === 0;`);
- ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`);
- ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
- ctx.addLine(`context.${name}_index = i;`);
- ctx.addLine(`context.${name} = _${keysID}[i];`);
- ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
- const nodeCopy = node.cloneNode(true);
- if (nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key")) {
- console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`);
- }
- nodeCopy.removeAttribute("t-foreach");
- qweb._compileNode(nodeCopy, ctx);
- ctx.dedent();
- ctx.addLine("}");
- return true;
- }
- };
- const onDirective = {
- name: "on",
- priority: 90,
- atNodeCreation({ ctx, fullName, value, nodeID }) {
- ctx.rootContext.shouldDefineOwner = true;
- const eventName = fullName.slice(5);
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handler = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`;
- if (extraArgs) {
- ctx.addLine(`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`);
- }
- else {
- ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`);
- ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
- }
- }
- };
- const refDirective = {
- name: "ref",
- priority: 95,
- atNodeCreation({ ctx, node }) {
- let ref = node.getAttribute("t-ref");
- ctx.addLine(`p${ctx.parentNode}.hook = {
- create: (_, n) => context.refs[${ctx.formatExpression(ref)}] = n.elm,
- };`);
- }
- };
- const widgetDirective = {
- name: "widget",
- priority: 100,
- atNodeEncounter({ ctx, value, node }) {
- ctx.addLine("//WIDGET");
- ctx.rootContext.shouldDefineOwner = true;
- let props = node.getAttribute("t-props");
- let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- // t-on- events...
- const events = [];
- const attributes = node.attributes;
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- if (name.startsWith("t-on-")) {
- events.push([name.slice(5), attributes[i].textContent]);
- }
- }
- let key = node.getAttribute("t-key");
- if (key) {
- key = ctx.formatExpression(key);
- }
- if (props) {
- props = ctx.formatExpression(props);
- }
- let dummyID = ctx.generateID();
- let defID = ctx.generateID();
- let widgetID = ctx.generateID();
- let keyID = key && ctx.generateID();
- if (key) {
- // we bind a variable to the key (could be a complex expression, so we
- // want to evaluate it only once)
- ctx.addLine(`let key${keyID} = ${key};`);
- }
- ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
- ctx.addLine(`c${ctx.parentNode}.push(null);`);
- ctx.addLine(`let def${defID};`);
- let templateID = key
- ? `key${keyID}`
- : ctx.inLoop
- ? `String(-${widgetID} - i)`
- : String(widgetID);
- ctx.addLine(`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`);
- ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
- ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
- // check if we can reuse current rendering promise
- ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`);
- ctx.addIf(`w${widgetID}.__owl__.isStarted`);
- ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`);
- ctx.addElse();
- ctx.addLine(`isNew${widgetID} = true`);
- ctx.addIf(`props${widgetID} === w${widgetID}.__owl__.renderProps`);
- ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
- ctx.addElse();
- ctx.addLine(`w${widgetID}.destroy();`);
- ctx.addLine(`w${widgetID} = false`);
- ctx.closeIf();
- ctx.closeIf();
- ctx.closeIf();
- ctx.addIf(`!def${defID}`);
- ctx.addIf(`w${widgetID}`);
- ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`);
- ctx.addElse();
- ctx.addLine(`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`);
- ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`);
- for (let [event, method] of events) {
- ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
- }
- let ref = node.getAttribute("t-ref");
- let refExpr = ref ? `context.refs[${ctx.formatExpression(ref)}] = w${widgetID};` : '';
- ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
- ctx.closeIf();
- ctx.closeIf();
- ctx.addIf(`isNew${widgetID}`);
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){w${widgetID}.${keepAlive ? "unmount" : "destroy"}()},destroy(){w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}}; w${widgetID}.__owl__.pvnode = pvnode;});`);
- ctx.addElse();
- ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}, destroy() {w${widgetID}.${keepAlive ? "unmount" : "destroy"}()}}}c${ctx.parentNode}[_${dummyID}_index]=vnode;});`);
- ctx.closeIf();
- ctx.addLine(`extra.promises.push(def${defID});`);
- if (node.hasAttribute("t-if") || node.hasAttribute("t-else")) {
- ctx.closeIf();
- }
- return true;
- }
- };
-
- class Store extends EventBus {
- constructor(config, options = {}) {
- super();
- this._commitLevel = 0;
- this.history = [];
- this.debug = options.debug || false;
- this.state = config.state || {};
- this.actions = config.actions;
- this.mutations = config.mutations;
- this.env = config.env;
- this.observer = new Observer();
- this.observer.notifyCB = this.trigger.bind(this, "update");
- this.observer.allowMutations = false;
- this.observer.observe(this.state);
- if (this.debug) {
- this.history.push({ state: this.state });
- }
- this.set = this.observer.set.bind(this.observer);
- }
- dispatch(action, payload) {
- if (!this.actions[action]) {
- throw new Error(`[Error] action ${action} is undefined`);
- }
- const result = this.actions[action]({
- commit: this.commit.bind(this),
- dispatch: this.dispatch.bind(this),
- env: this.env,
- state: this.state
- }, payload);
- if (result instanceof Promise) {
- return new Promise((resolve, reject) => {
- result.then(() => resolve());
- result.catch(reject);
- });
- }
- }
- commit(type, payload) {
- if (!this.mutations[type]) {
- throw new Error(`[Error] mutation ${type} is undefined`);
- }
- this._commitLevel++;
- this.observer.allowMutations = true;
- const res = this.mutations[type].call(null, {
- commit: this.commit.bind(this),
- state: this.state,
- set: this.set
- }, payload);
- if (this._commitLevel === 1) {
- this.observer.allowMutations = false;
- if (this.debug) {
- this.history.push({
- state: this.state,
- mutation: type,
- payload: payload
- });
- }
- }
- this._commitLevel--;
- return res;
- }
- }
- //------------------------------------------------------------------------------
- // Connect function
- //------------------------------------------------------------------------------
- function revNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.rev;
- }
- return 0;
- }
- function deepRevNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.deepRev;
- }
- return 0;
- }
- function connect(mapStateToProps, options = {}) {
- let hashFunction = options.hashFunction || null;
- if (!hashFunction) {
- let deep = "deep" in options ? options.deep : true;
- let defaultRevFunction = deep ? deepRevNumber : revNumber;
- hashFunction = function ({ storeProps }, options) {
- const { currentStoreProps } = options;
- if ("__owl__" in storeProps) {
- return defaultRevFunction(storeProps);
- }
- let hash = 0;
- for (let key in storeProps) {
- const val = storeProps[key];
- const hashVal = defaultRevFunction(val);
- if (hashVal === 0) {
- if (val !== currentStoreProps[key]) {
- options.didChange = true;
- }
- }
- else {
- hash += hashVal;
- }
- }
- return hash;
- };
- }
- return function (Comp) {
- return class extends Comp {
- constructor(parent, props) {
- const env = parent instanceof Component ? parent.env : parent;
- const ownProps = Object.assign({}, props || {});
- const storeProps = mapStateToProps(env.store.state, ownProps);
- const mergedProps = Object.assign({}, props || {}, storeProps);
- super(parent, mergedProps);
- this.__owl__.ownProps = ownProps;
- this.__owl__.currentStoreProps = storeProps;
- this.__owl__.storeHash = hashFunction({
- state: env.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, {
- currentStoreProps: storeProps
- });
- }
- mounted() {
- this.env.store.on("update", this, () => {
- const ownProps = this.__owl__.ownProps;
- const storeProps = mapStateToProps(this.env.store.state, ownProps);
- const options = {
- currentStoreProps: this.__owl__.currentStoreProps
- };
- const storeHash = hashFunction({
- state: this.env.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, options);
- let didChange = options.didChange;
- if (storeHash !== this.__owl__.storeHash) {
- didChange = true;
- this.__owl__.storeHash = storeHash;
- }
- if (didChange) {
- this.__owl__.currentStoreProps = storeProps;
- this.updateProps(ownProps, false);
- }
- });
- super.mounted();
- }
- willUnmount() {
- this.env.store.off("update", this);
- super.willUnmount();
- }
- updateProps(nextProps, forceUpdate) {
- if (this.__owl__.ownProps !== nextProps) {
- this.__owl__.currentStoreProps = mapStateToProps(this.env.store.state, nextProps);
- }
- this.__owl__.ownProps = nextProps;
- const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps);
- return super.updateProps(mergedProps, forceUpdate);
- }
- };
- };
- }
-
- const utils = _utils;
-
- exports.utils = utils;
- exports.Component = Component;
- exports.EventBus = EventBus;
- exports.QWeb = QWeb;
- exports.connect = connect;
- exports.Store = Store;
-
- exports._version = '0.8.0';
- exports._date = '2019-04-24T08:12:21.164Z';
- exports._hash = 'b14d264';
- exports._url = 'https://github.com/odoo/owl';
-
-}(this.owl = this.owl || {}));
diff --git a/tools/benchmarks/owl-0.8.0/templates.xml b/tools/benchmarks/owl-0.8.0/templates.xml
deleted file mode 100644
index 699ebea3..00000000
--- a/tools/benchmarks/owl-0.8.0/templates.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Number of messages:
-
-
Log (clear)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/benchmarks/owl-0.14.0/app.js b/tools/benchmarks/owl-1.0.0-beta1/app.js
similarity index 83%
rename from tools/benchmarks/owl-0.14.0/app.js
rename to tools/benchmarks/owl-1.0.0-beta1/app.js
index e739aeb1..caa88b4b 100644
--- a/tools/benchmarks/owl-0.14.0/app.js
+++ b/tools/benchmarks/owl-1.0.0-beta1/app.js
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
+const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
- state = { counter: 0 };
+ state = useState({ counter: 0 });
increment() {
this.state.counter++;
@@ -15,8 +16,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
- widgets = { Counter };
-
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
@@ -26,13 +25,14 @@ class Message extends owl.Component {
});
}
}
+Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
- widgets = { Message };
- state = { messages: [], multipleFlag: false, clearAfterFlag: false };
+ state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
+ logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -108,10 +108,10 @@ class App extends owl.Component {
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
- for (let i = 0; i < this.state.messages.length; i += 10) {
+ for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
- this.set(messages, i, msg);
+ messages[i] = msg;
}
});
}
@@ -129,32 +129,25 @@ class App extends owl.Component {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
- this.refs.log.appendChild(div);
- this.refs.log.scrollTop = this.refs.log.scrollHeight;
+ this.logRef.el.appendChild(div);
+ this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
- this.refs.log.innerHTML = "";
- }
-
- toggleMultiple() {
- this.state.multipleFlag = !this.state.multipleFlag;
- }
-
- toggleClear() {
- this.state.clearAfterFlag = !this.state.clearAfterFlag;
+ this.logRef.el.innerHTML = "";
}
}
+App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
- const templates = await owl.utils.loadTemplates("templates.xml");
- const env = {
- qweb: new owl.QWeb(templates)
+ const templates = await owl.utils.loadFile("templates.xml");
+ App.env = {
+ qweb: new owl.QWeb({ templates })
};
- const app = new App(env);
+ const app = new App();
app.mount(document.body);
}
diff --git a/tools/benchmarks/owl-0.12.0/index.html b/tools/benchmarks/owl-1.0.0-beta1/index.html
similarity index 78%
rename from tools/benchmarks/owl-0.12.0/index.html
rename to tools/benchmarks/owl-1.0.0-beta1/index.html
index 66f1950e..13aa45ec 100644
--- a/tools/benchmarks/owl-0.12.0/index.html
+++ b/tools/benchmarks/owl-1.0.0-beta1/index.html
@@ -2,12 +2,11 @@
- OWL 0.12.0 Benchmark
+ OWL v1.0.0-beta1 Benchmark
-
diff --git a/tools/benchmarks/owl-0.16.0/owl.js b/tools/benchmarks/owl-1.0.0-beta1/owl.js
similarity index 54%
rename from tools/benchmarks/owl-0.16.0/owl.js
rename to tools/benchmarks/owl-1.0.0-beta1/owl.js
index fd8d41ed..dfbc49f5 100644
--- a/tools/benchmarks/owl-0.16.0/owl.js
+++ b/tools/benchmarks/owl-1.0.0-beta1/owl.js
@@ -1,6 +1,71 @@
(function (exports) {
'use strict';
+ /**
+ * We define here a simple event bus: it can
+ * - emit events
+ * - add/remove listeners.
+ *
+ * This is a useful pattern of communication in many cases. For OWL, each
+ * components and stores are event buses.
+ */
+ //------------------------------------------------------------------------------
+ // EventBus
+ //------------------------------------------------------------------------------
+ class EventBus {
+ constructor() {
+ this.subscriptions = {};
+ }
+ /**
+ * Add a listener for the 'eventType' events.
+ *
+ * Note that the 'owner' of this event can be anything, but will more likely
+ * be a component or a class. The idea is that the callback will be called with
+ * the proper owner bound.
+ *
+ * Also, the owner should be kind of unique. This will be used to remove the
+ * listener.
+ */
+ on(eventType, owner, callback) {
+ if (!callback) {
+ throw new Error("Missing callback");
+ }
+ if (!this.subscriptions[eventType]) {
+ this.subscriptions[eventType] = [];
+ }
+ this.subscriptions[eventType].push({
+ owner,
+ callback
+ });
+ }
+ /**
+ * Remove a listener
+ */
+ off(eventType, owner) {
+ const subs = this.subscriptions[eventType];
+ if (subs) {
+ this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
+ }
+ }
+ /**
+ * Emit an event of type 'eventType'. Any extra arguments will be passed to
+ * the listeners callback.
+ */
+ trigger(eventType, ...args) {
+ const subs = this.subscriptions[eventType] || [];
+ for (let i = 0, iLen = subs.length; i < iLen; i++) {
+ const sub = subs[i];
+ sub.callback.call(sub.owner, ...args);
+ }
+ }
+ /**
+ * Remove all subscriptions.
+ */
+ clear() {
+ this.subscriptions = {};
+ }
+ }
+
/**
* Owl Observer
*
@@ -10,55 +75,13 @@
* This is a Observer class that can observe any JS values. The way it works
* can be summarized thusly:
* - primitive values are not observed at all
- * - Objects are observed by replacing all their keys with getters/setters
- * (recursively)
- * - Arrays are observed by replacing their prototype with a customized version,
- * which wrap some methods to allow the tracking of each state change.
+ * - Objects and arrays are observed by replacing them with a Proxy
+ * - each object/array metadata are tracked in a weakmap, and keep a revision
+ * number
*
- * Note that this code is inspired by Vue.
+ * Note that this code is loosely inspired by Vue.
*/
//------------------------------------------------------------------------------
- // Modified Array prototype
- //------------------------------------------------------------------------------
- // we define here a new modified Array prototype, which basically override all
- // Array methods that change some state to be able to track their changes
- const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
- const methodLen = methodsToPatch.length;
- const ArrayProto = Array.prototype;
- const ModifiedArrayProto = Object.create(ArrayProto);
- for (let i = 0; i < methodLen; i++) {
- const method = methodsToPatch[i];
- const initialMethod = ArrayProto[method];
- ModifiedArrayProto[method] = function (...args) {
- if (!this.__observer__.allowMutations) {
- throw new Error(`Array cannot be changed here")`);
- }
- this.__observer__.rev++;
- this.__observer__.notifyChange();
- this.__owl__.rev++;
- let parent = this;
- do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent));
- let inserted;
- switch (method) {
- case "push":
- case "unshift":
- inserted = args;
- break;
- case "splice":
- inserted = args.slice(2);
- break;
- }
- if (inserted) {
- for (let i = 0, iLen = inserted.length; i < iLen; i++) {
- this.__observer__.observe(inserted[i], this);
- }
- }
- return initialMethod.call(this, ...args);
- };
- }
- //------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
class Observer {
@@ -66,123 +89,701 @@
this.rev = 1;
this.allowMutations = true;
this.dirty = false;
- }
- static set(target, key, value) {
- if (!target.__owl__) {
- throw Error("`Observer.set()` can only be called with observed Objects or Arrays");
- }
- target.__owl__.observer.set(target, key, value);
- }
- static delete(target, key) {
- if (!target.__owl__) {
- throw Error("`Observer.delete()` can only be called with observed Objects");
- }
- target.__owl__.observer.delete(target, key);
+ this.weakMap = new WeakMap();
}
notifyCB() { }
- notifyChange() {
- this.dirty = true;
- Promise.resolve().then(() => {
- if (this.dirty) {
- this.dirty = false;
- this.notifyCB();
- }
- });
- }
observe(value, parent) {
- if (value === null) {
+ if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
- return;
- }
- if (typeof value !== "object") {
- return;
- }
- if ("__owl__" in value) {
- // already observed
- value.__owl__.parent = parent;
- return;
- }
- if (Array.isArray(value)) {
- this._observeArr(value, parent);
- }
- else {
- this._observeObj(value, parent);
+ return value;
}
+ let metadata = this.weakMap.get(value) || this._observe(value, parent);
+ return metadata.proxy;
}
- set(target, key, value) {
- let alreadyDefined = key in target && Object.getOwnPropertyDescriptor(target, key).configurable === false;
- if (alreadyDefined) {
- target[key] = value;
- }
- else {
- this._addProp(target, key, value);
- this._updateRevNumber(target);
- }
- this.notifyChange();
+ revNumber(value) {
+ const metadata = this.weakMap.get(value);
+ return metadata ? metadata.rev : 0;
}
- delete(target, key) {
- delete target[key];
- this._updateRevNumber(target);
- this.notifyChange();
- }
- _observeObj(obj, parent) {
- obj.__owl__ = {
- rev: this.rev,
- deepRev: this.rev,
- parent,
- observer: this
- };
- Object.defineProperty(obj, "__owl__", { enumerable: false });
- for (let key in obj) {
- this._addProp(obj, key, obj[key]);
- }
- }
- _observeArr(arr, parent) {
- arr.__owl__ = {
- rev: this.rev,
- deepRev: this.rev,
- parent,
- observer: this
- };
- Object.defineProperty(arr, "__owl__", { enumerable: false });
- arr.__proto__ = Object.create(ModifiedArrayProto);
- arr.__proto__.__observer__ = this;
- for (let i = 0, iLen = arr.length; i < iLen; i++) {
- this.observe(arr[i], arr);
- }
- }
- _addProp(obj, key, value) {
+ _observe(value, parent) {
var self = this;
- Object.defineProperty(obj, key, {
- configurable: true,
- enumerable: true,
- get() {
- return value;
+ const proxy = new Proxy(value, {
+ get(target, k) {
+ const targetValue = target[k];
+ return self.observe(targetValue, value);
},
- set(newVal) {
+ set(target, key, newVal) {
+ const value = target[key];
if (newVal !== value) {
if (!self.allowMutations) {
throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`);
}
- self._updateRevNumber(obj);
- value = newVal;
- self.observe(newVal, obj);
- self.notifyChange();
+ self._updateRevNumber(target);
+ target[key] = newVal;
+ self.notifyCB();
}
+ return true;
+ },
+ deleteProperty(target, key) {
+ if (key in target) {
+ delete target[key];
+ self._updateRevNumber(target);
+ self.notifyCB();
+ }
+ return true;
}
});
- this.observe(value, obj);
+ const metadata = {
+ value,
+ proxy,
+ rev: this.rev,
+ parent
+ };
+ this.weakMap.set(value, metadata);
+ this.weakMap.set(metadata.proxy, metadata);
+ return metadata;
}
_updateRevNumber(target) {
this.rev++;
- target.__owl__.rev++;
+ let metadata = this.weakMap.get(target);
let parent = target;
do {
- parent.__owl__.deepRev++;
- } while ((parent = parent.__owl__.parent) && parent !== target);
+ metadata = this.weakMap.get(parent);
+ metadata.rev++;
+ } while ((parent = metadata.parent) && parent !== target);
}
}
+ /**
+ * Owl QWeb Expression Parser
+ *
+ * Owl needs in various contexts to be able to understand the structure of a
+ * string representing a javascript expression. The usual goal is to be able
+ * to rewrite some variables. For example, if a template has
+ *
+ * ```xml
+ * ...
+ * ```
+ *
+ * this needs to be translated in something like this:
+ *
+ * ```js
+ * if (context["computeSomething"]({val: context["state"].val})) { ... }
+ * ```
+ *
+ * This file contains the implementation of an extremely naive tokenizer/parser
+ * and evaluator for javascript expressions. The supported grammar is basically
+ * only expressive enough to understand the shape of objects, of arrays, and
+ * various operators.
+ */
+ //------------------------------------------------------------------------------
+ // Misc types, constants and helpers
+ //------------------------------------------------------------------------------
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
+ const WORD_REPLACEMENT = {
+ and: "&&",
+ or: "||",
+ gt: ">",
+ gte: ">=",
+ lt: "<",
+ lte: "<="
+ };
+ const STATIC_TOKEN_MAP = {
+ "{": "LEFT_BRACE",
+ "}": "RIGHT_BRACE",
+ "[": "LEFT_BRACKET",
+ "]": "RIGHT_BRACKET",
+ ":": "COLON",
+ ",": "COMMA",
+ "(": "LEFT_PAREN",
+ ")": "RIGHT_PAREN"
+ };
+ // note that the space after typeof is relevant. It makes sure that the formatted
+ // expression has a space after typeof
+ const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
+ let tokenizeString = function (expr) {
+ let s = expr[0];
+ let start = s;
+ if (s !== "'" && s !== '"') {
+ return false;
+ }
+ let i = 1;
+ let cur;
+ while (expr[i] && expr[i] !== start) {
+ cur = expr[i];
+ s += cur;
+ if (cur === "\\") {
+ i++;
+ cur = expr[i];
+ if (!cur) {
+ throw new Error("Invalid expression");
+ }
+ s += cur;
+ }
+ i++;
+ }
+ if (expr[i] !== start) {
+ throw new Error("Invalid expression");
+ }
+ s += start;
+ return { type: "VALUE", value: s };
+ };
+ let tokenizeNumber = function (expr) {
+ let s = expr[0];
+ if (s && s.match(/[0-9]/)) {
+ let i = 1;
+ while (expr[i] && expr[i].match(/[0-9]|\./)) {
+ s += expr[i];
+ i++;
+ }
+ return { type: "VALUE", value: s };
+ }
+ else {
+ return false;
+ }
+ };
+ let tokenizeSymbol = function (expr) {
+ let s = expr[0];
+ if (s && s.match(/[a-zA-Z_\$]/)) {
+ let i = 1;
+ while (expr[i] && expr[i].match(/\w/)) {
+ s += expr[i];
+ i++;
+ }
+ if (s in WORD_REPLACEMENT) {
+ return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
+ }
+ return { type: "SYMBOL", value: s };
+ }
+ else {
+ return false;
+ }
+ };
+ const tokenizeStatic = function (expr) {
+ const char = expr[0];
+ if (char && char in STATIC_TOKEN_MAP) {
+ return { type: STATIC_TOKEN_MAP[char], value: char };
+ }
+ return false;
+ };
+ const tokenizeOperator = function (expr) {
+ for (let op of OPERATORS) {
+ if (expr.startsWith(op)) {
+ return { type: "OPERATOR", value: op };
+ }
+ }
+ return false;
+ };
+ const TOKENIZERS = [
+ tokenizeString,
+ tokenizeNumber,
+ tokenizeOperator,
+ tokenizeSymbol,
+ tokenizeStatic
+ ];
+ /**
+ * Convert a javascript expression (as a string) into a list of tokens. For
+ * example: `tokenize("1 + b")` will return:
+ * ```js
+ * [
+ * {type: "VALUE", value: "1"},
+ * {type: "OPERATOR", value: "+"},
+ * {type: "SYMBOL", value: "b"}
+ * ]
+ * ```
+ */
+ function tokenize(expr) {
+ const result = [];
+ let token = true;
+ while (token) {
+ expr = expr.trim();
+ if (expr) {
+ for (let tokenizer of TOKENIZERS) {
+ token = tokenizer(expr);
+ if (token) {
+ result.push(token);
+ expr = expr.slice(token.size || token.value.length);
+ break;
+ }
+ }
+ }
+ else {
+ token = false;
+ }
+ }
+ if (expr.length) {
+ throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
+ }
+ return result;
+ }
+ //------------------------------------------------------------------------------
+ // Expression "evaluator"
+ //------------------------------------------------------------------------------
+ /**
+ * This is the main function exported by this file. This is the code that will
+ * process an expression (given as a string) and returns another expression with
+ * proper lookups in the context.
+ *
+ * Usually, this kind of code would be very simple to do if we had an AST (so,
+ * if we had a javascript parser), since then, we would only need to find the
+ * variables and replace them. However, a parser is more complicated, and there
+ * are no standard builtin parser API.
+ *
+ * Since this method is applied to simple javasript expressions, and the work to
+ * be done is actually quite simple, we actually can get away with not using a
+ * parser, which helps with the code size.
+ *
+ * Here is the heuristic used by this method to determine if a token is a
+ * variable:
+ * - by default, all symbols are considered a variable
+ * - unless the previous token is a dot (in that case, this is a property: `a.b`)
+ * - or if the previous token is a left brace or a comma, and the next token is
+ * a colon (in that case, this is an object key: `{a: b}`)
+ */
+ function compileExpr(expr, vars) {
+ const tokens = tokenize(expr);
+ let result = "";
+ for (let i = 0; i < tokens.length; i++) {
+ let token = tokens[i];
+ if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
+ // we need to find if it is a variable
+ let isVar = true;
+ let prevToken = tokens[i - 1];
+ if (prevToken) {
+ if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
+ isVar = false;
+ }
+ else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
+ let nextToken = tokens[i + 1];
+ if (nextToken && nextToken.type === "COLON") {
+ isVar = false;
+ }
+ }
+ }
+ if (isVar) {
+ if (token.value in vars && "id" in vars[token.value]) {
+ token.value = vars[token.value].id;
+ }
+ else {
+ token.value = `context['${token.value}']`;
+ }
+ }
+ }
+ result += token.value;
+ }
+ return result;
+ }
+
+ const INTERP_REGEXP = /\{\{.*?\}\}/g;
+ //------------------------------------------------------------------------------
+ // Compilation Context
+ //------------------------------------------------------------------------------
+ class CompilationContext {
+ constructor(name) {
+ this.code = [];
+ this.variables = {};
+ this.escaping = false;
+ this.parentNode = null;
+ this.parentTextNode = null;
+ this.rootNode = null;
+ this.indentLevel = 0;
+ this.shouldDefineOwner = false;
+ this.shouldDefineParent = false;
+ this.shouldDefineQWeb = false;
+ this.shouldDefineUtils = false;
+ this.shouldDefineRefs = false;
+ this.shouldDefineResult = true;
+ this.shouldProtectContext = false;
+ this.shouldTrackScope = false;
+ this.loopNumber = 0;
+ this.inPreTag = false;
+ this.allowMultipleRoots = false;
+ this.hasParentWidget = false;
+ this.scopeVars = [];
+ this.currentKey = "";
+ this.templates = {};
+ this.callingLevel = 0;
+ this.inliningLevel = 0;
+ this.rootContext = this;
+ this.templateName = name || "noname";
+ this.templates[this.templateName] = true;
+ this.addLine("var h = this.h;");
+ }
+ generateID() {
+ return CompilationContext.nextID++;
+ }
+ /**
+ * This method generates a "template key", which is basically a unique key
+ * which depends on the currently set keys, and on the iteration numbers (if
+ * we are in a loop).
+ *
+ * Such a key is necessary when we need to associate an id to some element
+ * generated by a template (for example, a component)
+ */
+ generateTemplateKey() {
+ const id = this.generateID();
+ let locationExpr = `\`__${this.generateID()}__`;
+ for (let i = 0; i < this.loopNumber - 1; i++) {
+ locationExpr += `\${i${i + 1}}__`;
+ }
+ if (this.currentKey) {
+ const k = this.currentKey;
+ this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
+ }
+ else {
+ locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
+ this.addLine(`let k${id} = ${locationExpr};`);
+ }
+ return `k${id}`;
+ }
+ generateCode() {
+ const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
+ if (shouldTrackScope) {
+ // add some vars to scope if needed
+ for (let scopeVar of this.scopeVars.reverse()) {
+ let { index, key, indent } = scopeVar;
+ const prefix = new Array(indent + 2).join(" ");
+ this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
+ }
+ this.code.unshift(" const scope = Object.create(null);");
+ }
+ if (this.shouldProtectContext) {
+ this.code.unshift(" context = Object.create(context);");
+ }
+ if (this.shouldDefineResult) {
+ this.code.unshift(" let result;");
+ }
+ if (this.shouldDefineRefs) {
+ this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
+ }
+ if (this.shouldDefineOwner) {
+ // this is necessary to prevent some directives (t-forach for ex) to
+ // pollute the rendering context by adding some keys in it.
+ this.code.unshift(" let owner = context;");
+ }
+ if (this.shouldDefineParent) {
+ if (this.hasParentWidget) {
+ this.code.unshift(" let parent = extra.parent;");
+ }
+ else {
+ this.code.unshift(" let parent = context;");
+ }
+ }
+ if (this.shouldDefineQWeb) {
+ this.code.unshift(" let QWeb = this.constructor;");
+ }
+ if (this.shouldDefineUtils) {
+ this.code.unshift(" let utils = this.constructor.utils;");
+ }
+ return this.code;
+ }
+ withParent(node) {
+ if (!this.allowMultipleRoots &&
+ this === this.rootContext &&
+ (this.parentNode || this.parentTextNode)) {
+ throw new Error("A template should not have more than one root node");
+ }
+ if (!this.rootContext.rootNode) {
+ this.rootContext.rootNode = node;
+ }
+ if (!this.parentNode && this.rootContext.shouldDefineResult) {
+ this.addLine(`result = vn${node};`);
+ }
+ return this.subContext("parentNode", node);
+ }
+ subContext(key, value) {
+ const newContext = Object.create(this);
+ newContext[key] = value;
+ if (key === "caller") {
+ newContext.callingLevel++;
+ newContext.inliningLevel++;
+ }
+ return newContext;
+ }
+ indent() {
+ this.indentLevel++;
+ }
+ dedent() {
+ this.indentLevel--;
+ }
+ addLine(line) {
+ const prefix = new Array(this.indentLevel + 2).join(" ");
+ this.code.push(prefix + line);
+ return this.code.length - 1;
+ }
+ addToScope(key, expr) {
+ const index = this.addLine(`context.${key} = ${expr};`);
+ this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
+ }
+ addIf(condition) {
+ this.addLine(`if (${condition}) {`);
+ this.indent();
+ }
+ addElse() {
+ this.dedent();
+ this.addLine("} else {");
+ this.indent();
+ }
+ closeIf() {
+ this.dedent();
+ this.addLine("}");
+ }
+ /**
+ * Recursively (inverse) fetches the `caller` of a context
+ * Useful to determine to which t-call a t-raw="0" refers
+ */
+ getCaller(targetLevel) {
+ if (targetLevel === undefined) {
+ targetLevel = this.inliningLevel;
+ }
+ if (targetLevel === this.callingLevel) {
+ return this.caller || null;
+ }
+ const proto = this.__proto__;
+ return proto ? proto.getCaller(targetLevel) : null;
+ }
+ /**
+ * Marks the context with the current recursive level
+ * in which we are for inlining archs (t-raw="0")
+ */
+ getInliningContext() {
+ return this.subContext("inliningLevel", this.inliningLevel - 1);
+ }
+ getValue(val) {
+ return val in this.variables ? this.getValue(this.variables[val]) : val;
+ }
+ /**
+ * Prepare an expression for being consumed at render time. Its main job
+ * is to
+ * - replace unknown variables by a lookup in the context
+ * - replace already defined variables by their internal name
+ */
+ formatExpression(expr) {
+ return compileExpr(expr, this.variables);
+ }
+ /**
+ * Perform string interpolation on the given string. Note that if the whole
+ * string is an expression, it simply returns it (formatted and enclosed in
+ * parentheses).
+ * For instance:
+ * 'Hello {{x}}!' -> `Hello ${x}`
+ * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
+ */
+ interpolate(s) {
+ let matches = s.match(INTERP_REGEXP);
+ if (matches && matches[0].length === s.length) {
+ return `(${this.formatExpression(s.slice(2, -2))})`;
+ }
+ let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
+ return "`" + r + "`";
+ }
+ }
+ CompilationContext.nextID = 1;
+
+ //------------------------------------------------------------------------------
+ // module/props.ts
+ //------------------------------------------------------------------------------
+ function updateProps(oldVnode, vnode) {
+ var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
+ if (!oldProps && !props)
+ return;
+ if (oldProps === props)
+ return;
+ oldProps = oldProps || {};
+ props = props || {};
+ for (key in oldProps) {
+ if (!props[key]) {
+ delete elm[key];
+ }
+ }
+ for (key in props) {
+ cur = props[key];
+ old = oldProps[key];
+ if (old !== cur && (key !== "value" || elm[key] !== cur)) {
+ elm[key] = cur;
+ }
+ }
+ }
+ const propsModule = {
+ create: updateProps,
+ update: updateProps
+ };
+ //------------------------------------------------------------------------------
+ // module/eventlisteners.ts
+ //------------------------------------------------------------------------------
+ function invokeHandler(handler, vnode, event) {
+ if (typeof handler === "function") {
+ // call function handler
+ handler.call(vnode, event, vnode);
+ }
+ else if (typeof handler === "object") {
+ // call handler with arguments
+ if (typeof handler[0] === "function") {
+ // special case for single argument for performance
+ if (handler.length === 2) {
+ handler[0].call(vnode, handler[1], event, vnode);
+ }
+ else {
+ var args = handler.slice(1);
+ args.push(event);
+ args.push(vnode);
+ handler[0].apply(vnode, args);
+ }
+ }
+ else {
+ // call multiple handlers
+ for (let i = 0, iLen = handler.length; i < iLen; i++) {
+ invokeHandler(handler[i], vnode, event);
+ }
+ }
+ }
+ }
+ function handleEvent(event, vnode) {
+ var name = event.type, on = vnode.data.on;
+ // call event handler(s) if exists
+ if (on && on[name]) {
+ invokeHandler(on[name], vnode, event);
+ }
+ }
+ function createListener() {
+ return function handler(event) {
+ handleEvent(event, handler.vnode);
+ };
+ }
+ function updateEventListeners(oldVnode, vnode) {
+ var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name;
+ // optimization for reused immutable handlers
+ if (oldOn === on) {
+ return;
+ }
+ // remove existing listeners which no longer used
+ if (oldOn && oldListener) {
+ // if element changed or deleted we remove all existing listeners unconditionally
+ if (!on) {
+ for (name in oldOn) {
+ // remove listener if element was changed or existing listeners removed
+ oldElm.removeEventListener(name, oldListener, false);
+ }
+ }
+ else {
+ for (name in oldOn) {
+ // remove listener if existing listener removed
+ if (!on[name]) {
+ oldElm.removeEventListener(name, oldListener, false);
+ }
+ }
+ }
+ }
+ // add new listeners which has not already attached
+ if (on) {
+ // reuse existing listener or create new
+ var listener = (vnode.listener = oldVnode.listener || createListener());
+ // update vnode for listener
+ listener.vnode = vnode;
+ // if element changed or added we add all needed listeners unconditionally
+ if (!oldOn) {
+ for (name in on) {
+ // add listener if element was changed or new listeners added
+ elm.addEventListener(name, listener, false);
+ }
+ }
+ else {
+ for (name in on) {
+ // add listener if new listener added
+ if (!oldOn[name]) {
+ elm.addEventListener(name, listener, false);
+ }
+ }
+ }
+ }
+ }
+ const eventListenersModule = {
+ create: updateEventListeners,
+ update: updateEventListeners,
+ destroy: updateEventListeners
+ };
+ //------------------------------------------------------------------------------
+ // attributes.ts
+ //------------------------------------------------------------------------------
+ const xlinkNS = "http://www.w3.org/1999/xlink";
+ const xmlNS = "http://www.w3.org/XML/1998/namespace";
+ const colonChar = 58;
+ const xChar = 120;
+ function updateAttrs(oldVnode, vnode) {
+ var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs;
+ if (!oldAttrs && !attrs)
+ return;
+ if (oldAttrs === attrs)
+ return;
+ oldAttrs = oldAttrs || {};
+ attrs = attrs || {};
+ // update modified attributes, add new attributes
+ for (key in attrs) {
+ const cur = attrs[key];
+ const old = oldAttrs[key];
+ if (old !== cur) {
+ if (cur === true) {
+ elm.setAttribute(key, "");
+ }
+ else if (cur === false) {
+ elm.removeAttribute(key);
+ }
+ else {
+ if (key.charCodeAt(0) !== xChar) {
+ elm.setAttribute(key, cur);
+ }
+ else if (key.charCodeAt(3) === colonChar) {
+ // Assume xml namespace
+ elm.setAttributeNS(xmlNS, key, cur);
+ }
+ else if (key.charCodeAt(5) === colonChar) {
+ // Assume xlink namespace
+ elm.setAttributeNS(xlinkNS, key, cur);
+ }
+ else {
+ elm.setAttribute(key, cur);
+ }
+ }
+ }
+ }
+ // remove removed attributes
+ // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
+ // the other option is to remove all attributes with value == undefined
+ for (key in oldAttrs) {
+ if (!(key in attrs)) {
+ elm.removeAttribute(key);
+ }
+ }
+ }
+ const attrsModule = {
+ create: updateAttrs,
+ update: updateAttrs
+ };
+ //------------------------------------------------------------------------------
+ // class.ts
+ //------------------------------------------------------------------------------
+ function updateClass(oldVnode, vnode) {
+ var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class;
+ if (!oldClass && !klass)
+ return;
+ if (oldClass === klass)
+ return;
+ oldClass = oldClass || {};
+ klass = klass || {};
+ elm = vnode.elm;
+ for (name in oldClass) {
+ if (!klass[name]) {
+ elm.classList.remove(name);
+ }
+ }
+ for (name in klass) {
+ cur = klass[name];
+ if (cur !== oldClass[name]) {
+ elm.classList[cur ? "add" : "remove"](name);
+ }
+ }
+ }
+ const classModule = { create: updateClass, update: updateClass };
+
/**
* Owl VDOM
*
@@ -273,20 +874,11 @@
vnode.elm = api.createComment(vnode.text);
}
else if (sel !== undefined) {
- // Parse selector
- const hashIdx = sel.indexOf("#");
- const dotIdx = sel.indexOf(".", hashIdx);
- const hash = hashIdx > 0 ? hashIdx : sel.length;
- const dot = dotIdx > 0 ? dotIdx : sel.length;
- const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
- const elm = (vnode.elm =
- isDef(data) && isDef((i = data.ns))
- ? api.createElementNS(i, tag)
- : api.createElement(tag));
- if (hash < dot)
- elm.setAttribute("id", sel.slice(hash + 1, dot));
- if (dotIdx > 0)
- elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
+ const elm = vnode.elm ||
+ (vnode.elm =
+ isDef(data) && isDef((i = data.ns))
+ ? api.createElementNS(i, sel)
+ : api.createElement(sel));
for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
cbs.create[i](emptyNode, vnode);
if (array(children)) {
@@ -557,18 +1149,6 @@
function setTextContent(node, text) {
node.textContent = text;
}
- function getTextContent(node) {
- return node.textContent;
- }
- function isElement(node) {
- return node.nodeType === 1;
- }
- function isText(node) {
- return node.nodeType === 3;
- }
- function isComment(node) {
- return node.nodeType === 8;
- }
const htmlDomApi = {
createElement,
createElementNS,
@@ -580,19 +1160,21 @@
parentNode,
nextSibling,
tagName,
- setTextContent,
- getTextContent,
- isElement,
- isText,
- isComment
+ setTextContent
};
function addNS(data, children, sel) {
+ if (sel === "dummy") {
+ // we do not need to add the namespace on dummy elements, they come from a
+ // subcomponent, which will handle the namespace itself
+ return;
+ }
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) {
- let childData = children[i].data;
+ const child = children[i];
+ let childData = child.data;
if (childData !== undefined) {
- addNS(childData, children[i].children, children[i].sel);
+ addNS(childData, child.children, child.sel);
}
}
}
@@ -631,517 +1213,134 @@
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
- if (sel[0] === "s" &&
- sel[1] === "v" &&
- sel[2] === "g" &&
- (sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
- addNS(data, children, sel);
- }
return vnode(sel, data, children, text, undefined);
}
- function updateProps(oldVnode, vnode) {
- var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
- if (!oldProps && !props)
- return;
- if (oldProps === props)
- return;
- oldProps = oldProps || {};
- props = props || {};
- for (key in oldProps) {
- if (!props[key]) {
- delete elm[key];
- }
- }
- for (key in props) {
- cur = props[key];
- old = oldProps[key];
- if (old !== cur && (key !== "value" || elm[key] !== cur)) {
- elm[key] = cur;
- }
- }
- }
- const propsModule = {
- create: updateProps,
- update: updateProps
- };
- function invokeHandler(handler, vnode, event) {
- if (typeof handler === "function") {
- // call function handler
- handler.call(vnode, event, vnode);
- }
- else if (typeof handler === "object") {
- // call handler with arguments
- if (typeof handler[0] === "function") {
- // special case for single argument for performance
- if (handler.length === 2) {
- handler[0].call(vnode, handler[1], event, vnode);
- }
- else {
- var args = handler.slice(1);
- args.push(event);
- args.push(vnode);
- handler[0].apply(vnode, args);
- }
- }
- else {
- // call multiple handlers
- for (let i = 0, iLen = handler.length; i < iLen; i++) {
- invokeHandler(handler[i], vnode, event);
- }
- }
- }
- }
- function handleEvent(event, vnode) {
- var name = event.type, on = vnode.data.on;
- // call event handler(s) if exists
- if (on && on[name]) {
- invokeHandler(on[name], vnode, event);
- }
- }
- function createListener() {
- return function handler(event) {
- handleEvent(event, handler.vnode);
- };
- }
- function updateEventListeners(oldVnode, vnode) {
- var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name;
- // optimization for reused immutable handlers
- if (oldOn === on) {
- return;
- }
- // remove existing listeners which no longer used
- if (oldOn && oldListener) {
- // if element changed or deleted we remove all existing listeners unconditionally
- if (!on) {
- for (name in oldOn) {
- // remove listener if element was changed or existing listeners removed
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- else {
- for (name in oldOn) {
- // remove listener if existing listener removed
- if (!on[name]) {
- oldElm.removeEventListener(name, oldListener, false);
- }
- }
- }
- }
- // add new listeners which has not already attached
- if (on) {
- // reuse existing listener or create new
- var listener = (vnode.listener = oldVnode.listener || createListener());
- // update vnode for listener
- listener.vnode = vnode;
- // if element changed or added we add all needed listeners unconditionally
- if (!oldOn) {
- for (name in on) {
- // add listener if element was changed or new listeners added
- elm.addEventListener(name, listener, false);
- }
- }
- else {
- for (name in on) {
- // add listener if new listener added
- if (!oldOn[name]) {
- elm.addEventListener(name, listener, false);
- }
- }
- }
- }
- }
- const eventListenersModule = {
- create: updateEventListeners,
- update: updateEventListeners,
- destroy: updateEventListeners
- };
- const xlinkNS = "http://www.w3.org/1999/xlink";
- const xmlNS = "http://www.w3.org/XML/1998/namespace";
- const colonChar = 58;
- const xChar = 120;
- function updateAttrs(oldVnode, vnode) {
- var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs;
- if (!oldAttrs && !attrs)
- return;
- if (oldAttrs === attrs)
- return;
- oldAttrs = oldAttrs || {};
- attrs = attrs || {};
- // update modified attributes, add new attributes
- for (key in attrs) {
- const cur = attrs[key];
- const old = oldAttrs[key];
- if (old !== cur) {
- if (cur === true) {
- elm.setAttribute(key, "");
- }
- else if (cur === false) {
- elm.removeAttribute(key);
- }
- else {
- if (key.charCodeAt(0) !== xChar) {
- elm.setAttribute(key, cur);
- }
- else if (key.charCodeAt(3) === colonChar) {
- // Assume xml namespace
- elm.setAttributeNS(xmlNS, key, cur);
- }
- else if (key.charCodeAt(5) === colonChar) {
- // Assume xlink namespace
- elm.setAttributeNS(xlinkNS, key, cur);
- }
- else {
- elm.setAttribute(key, cur);
- }
- }
- }
- }
- // remove removed attributes
- // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
- // the other option is to remove all attributes with value == undefined
- for (key in oldAttrs) {
- if (!(key in attrs)) {
- elm.removeAttribute(key);
- }
- }
- }
- const attrsModule = {
- create: updateAttrs,
- update: updateAttrs
- };
- //------------------------------------------------------------------------------
- // class.ts
- //------------------------------------------------------------------------------
- function updateClass(oldVnode, vnode) {
- var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class;
- if (!oldClass && !klass)
- return;
- if (oldClass === klass)
- return;
- oldClass = oldClass || {};
- klass = klass || {};
- elm = vnode.elm;
- for (name in oldClass) {
- if (!klass[name]) {
- elm.classList.remove(name);
- }
- }
- for (name in klass) {
- cur = klass[name];
- if (cur !== oldClass[name]) {
- elm.classList[cur ? "add" : "remove"](name);
- }
- }
- }
- const classModule = { create: updateClass, update: updateClass };
- //------------------------------------------------------------------------------
- // patch
- //------------------------------------------------------------------------------
+
const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
/**
- * Owl QWeb Expression Parser
+ * Owl Utils
*
- * Owl needs in various contexts to be able to understand the structure of a
- * string representing a javascript expression. The usual goal is to be able
- * to rewrite some variables. For example, if a template has
+ * We have here a small collection of utility functions:
*
- * ```xml
- * ...
- * ```
- *
- * this needs to be translated in something like this:
- *
- * ```js
- * if (context["computeSomething"]({val: context["state"].val})) { ... }
- * ```
- *
- * This file contains the implementation of an extremely naive tokenizer/parser
- * and evaluator for javascript expressions. The supported grammar is basically
- * only expressive enough to understand the shape of objects, of arrays, and
- * various operators.
+ * - whenReady
+ * - loadJS
+ * - loadFile
+ * - escape
+ * - debounce
*/
- //------------------------------------------------------------------------------
- // Misc types, constants and helpers
- //------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const STATIC_TOKEN_MAP = {
- "{": "LEFT_BRACE",
- "}": "RIGHT_BRACE",
- "[": "LEFT_BRACKET",
- "]": "RIGHT_BRACKET",
- ":": "COLON",
- ",": "COMMA",
- "(": "LEFT_PAREN",
- ")": "RIGHT_PAREN"
- };
- const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
- let tokenizeString = function (expr) {
- let s = expr[0];
- let start = s;
- if (s !== "'" && s !== '"') {
- return false;
- }
- let i = 1;
- let cur;
- while (expr[i] && expr[i] !== start) {
- cur = expr[i];
- s += cur;
- if (cur === "\\") {
- i++;
- cur = expr[i];
- if (!cur) {
- throw new Error("Invalid expression");
- }
- s += cur;
- }
- i++;
- }
- if (expr[i] !== start) {
- throw new Error("Invalid expression");
- }
- s += start;
- return { type: "VALUE", value: s };
- };
- let tokenizeNumber = function (expr) {
- let s = expr[0];
- if (s && s.match(/[0-9]/)) {
- let i = 1;
- while (expr[i] && expr[i].match(/[0-9]|\./)) {
- s += expr[i];
- i++;
- }
- return { type: "VALUE", value: s };
- }
- else {
- return false;
- }
- };
- let tokenizeSymbol = function (expr) {
- let s = expr[0];
- if (s && s.match(/[a-zA-Z_\$]/)) {
- let i = 1;
- while (expr[i] && expr[i].match(/\w/)) {
- s += expr[i];
- i++;
- }
- if (s in WORD_REPLACEMENT) {
- return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
- }
- return { type: "SYMBOL", value: s };
- }
- else {
- return false;
- }
- };
- const tokenizeStatic = function (expr) {
- const char = expr[0];
- if (char && char in STATIC_TOKEN_MAP) {
- return { type: STATIC_TOKEN_MAP[char], value: char };
- }
- return false;
- };
- const tokenizeOperator = function (expr) {
- for (let op of OPERATORS) {
- if (expr.startsWith(op)) {
- return { type: "OPERATOR", value: op };
- }
- }
- return false;
- };
- const TOKENIZERS = [
- tokenizeString,
- tokenizeNumber,
- tokenizeSymbol,
- tokenizeStatic,
- tokenizeOperator
- ];
- /**
- * Convert a javascript expression (as a string) into a list of tokens. For
- * example: `tokenize("1 + b")` will return:
- * ```js
- * [
- * {type: "VALUE", value: "1"},
- * {type: "OPERATOR", value: "+"},
- * {type: "SYMBOL", value: "b"}
- * ]
- * ```
- */
- function tokenize(expr) {
- const result = [];
- let token = true;
- while (token) {
- expr = expr.trim();
- if (expr) {
- for (let tokenizer of TOKENIZERS) {
- token = tokenizer(expr);
- if (token) {
- result.push(token);
- expr = expr.slice(token.size || token.value.length);
- break;
- }
- }
+ function whenReady(fn) {
+ return new Promise(function (resolve) {
+ if (document.readyState !== "loading") {
+ resolve();
}
else {
- token = false;
+ document.addEventListener("DOMContentLoaded", resolve, false);
}
- }
- if (expr.length) {
- throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
- }
- return result;
+ }).then(fn || function () { });
+ }
+ const loadedScripts = {};
+ function loadJS(url) {
+ if (url in loadedScripts) {
+ return loadedScripts[url];
+ }
+ const promise = new Promise(function (resolve, reject) {
+ const script = document.createElement("script");
+ script.type = "text/javascript";
+ script.src = url;
+ script.onload = function () {
+ resolve();
+ };
+ script.onerror = function () {
+ reject(`Error loading file '${url}'`);
+ };
+ const head = document.head || document.getElementsByTagName("head")[0];
+ head.appendChild(script);
+ });
+ loadedScripts[url] = promise;
+ return promise;
+ }
+ async function loadFile(url) {
+ const result = await fetch(url);
+ if (!result.ok) {
+ throw new Error("Error while fetching xml templates");
+ }
+ return await result.text();
+ }
+ function escape(str) {
+ if (str === undefined) {
+ return "";
+ }
+ if (typeof str === "number") {
+ return String(str);
+ }
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, "'")
+ .replace(/`/g, "`");
}
- //------------------------------------------------------------------------------
- // Expression "evaluator"
- //------------------------------------------------------------------------------
/**
- * This is the main function exported by this file. This is the code that will
- * process an expression (given as a string) and returns another expression with
- * proper lookups in the context.
+ * Returns a function, that, as long as it continues to be invoked, will not
+ * be triggered. The function will be called after it stops being called for
+ * N milliseconds. If `immediate` is passed, trigger the function on the
+ * leading edge, instead of the trailing.
*
- * Usually, this kind of code would be very simple to do if we had an AST (so,
- * if we had a javascript parser), since then, we would only need to find the
- * variables and replace them. However, a parser is more complicated, and there
- * are no standard builtin parser API.
- *
- * Since this method is applied to simple javasript expressions, and the work to
- * be done is actually quite simple, we actually can get away with not using a
- * parser, which helps with the code size.
- *
- * Here is the heuristic used by this method to determine if a token is a
- * variable:
- * - by default, all symbols are considered a variable
- * - unless the previous token is a dot (in that case, this is a property: `a.b`)
- * - or if the previous token is a left brace or a comma, and the next token is
- * a colon (in that case, this is an object key: `{a: b}`)
+ * Inspired by https://davidwalsh.name/javascript-debounce-function
*/
- function compileExpr(expr, vars) {
- const tokens = tokenize(expr);
- let result = "";
- for (let i = 0; i < tokens.length; i++) {
- let token = tokens[i];
- if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
- // we need to find if it is a variable
- let isVar = true;
- let prevToken = tokens[i - 1];
- if (prevToken) {
- if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
- isVar = false;
- }
- else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
- let nextToken = tokens[i + 1];
- if (nextToken && nextToken.type === "COLON") {
- isVar = false;
- }
- }
- }
- if (isVar) {
- if (token.value in vars && "id" in vars[token.value]) {
- token.value = vars[token.value].id;
- }
- else {
- token.value = `context['${token.value}']`;
- }
+ function debounce(func, wait, immediate) {
+ let timeout;
+ return function () {
+ const context = this;
+ const args = arguments;
+ function later() {
+ timeout = null;
+ if (!immediate) {
+ func.apply(context, args);
}
}
- result += token.value;
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ func.apply(context, args);
+ }
+ };
+ }
+ function shallowEqual(p1, p2) {
+ for (let k in p1) {
+ if (p1[k] !== p2[k]) {
+ return false;
+ }
}
- return result;
+ return true;
}
- /**
- * We define here a simple event bus: it can
- * - emit events
- * - add/remove listeners.
- *
- * This is a useful pattern of communication in many cases. For OWL, each
- * components and stores are event buses.
- */
- //------------------------------------------------------------------------------
- // EventBus
- //------------------------------------------------------------------------------
- class EventBus {
- constructor() {
- this.subscriptions = {};
- }
- /**
- * Add a listener for the 'eventType' events.
- *
- * Note that the 'owner' of this event can be anything, but will more likely
- * be a component or a class. The idea is that the callback will be called with
- * the proper owner bound.
- *
- * Also, the owner should be kind of unique. This will be used to remove the
- * listener.
- */
- on(eventType, owner, callback) {
- if (!callback) {
- throw new Error("Missing callback");
- }
- if (!this.subscriptions[eventType]) {
- this.subscriptions[eventType] = [];
- }
- this.subscriptions[eventType].push({
- owner,
- callback
- });
- }
- /**
- * Remove a listener
- */
- off(eventType, owner) {
- const subs = this.subscriptions[eventType];
- if (subs) {
- this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
- }
- }
- /**
- * Emit an event of type 'eventType'. Any extra arguments will be passed to
- * the listeners callback.
- */
- trigger(eventType, ...args) {
- const subs = this.subscriptions[eventType] || [];
- for (let i = 0, iLen = subs.length; i < iLen; i++) {
- const sub = subs[i];
- sub.callback.call(sub.owner, ...args);
- }
- }
- /**
- * Remove all subscriptions.
- */
- clear() {
- this.subscriptions = {};
- }
- }
+ var _utils = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ whenReady: whenReady,
+ loadJS: loadJS,
+ loadFile: loadFile,
+ escape: escape,
+ debounce: debounce,
+ shallowEqual: shallowEqual
+ });
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
+ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
- const DIRECTIVE_NAMES = {
- name: 1,
- att: 1,
- attf: 1,
- key: 1
- };
- const DIRECTIVES = [];
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
- remove: "(vn, rm)"
+ remove: "(vn, rm)",
+ destroy: "()"
};
const UTILS = {
- h: h,
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
@@ -1157,58 +1356,89 @@
}
return expr;
},
- shallowEqual(p1, p2) {
- for (let k in p1) {
- if (p1[k] !== p2[k]) {
- return false;
- }
- }
- return true;
+ shallowEqual,
+ addNameSpace(vnode) {
+ addNS(vnode.data, vnode.children, vnode.sel);
}
};
function parseXML(xml) {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
+ let msg = "Invalid XML in template.";
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
+ if (parsererrorText) {
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
+ const re = /\d+/g;
+ const firstMatch = re.exec(parsererrorText);
+ if (firstMatch) {
+ const lineNumber = Number(firstMatch[0]);
+ const line = xml.split("\n")[lineNumber - 1];
+ const secondMatch = re.exec(parsererrorText);
+ if (line && secondMatch) {
+ const columnIndex = Number(secondMatch[0]) - 1;
+ if (line[columnIndex]) {
+ msg +=
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
+ }
+ }
+ }
+ }
+ throw new Error(msg);
}
return doc;
}
- let nextID = 1;
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
class QWeb extends EventBus {
- constructor(data) {
+ constructor(config = {}) {
super();
- this.templates = {};
- this.utils = UTILS;
- // the id field is useful to be able to hash qweb instances. The current
- // use case is that component's templates are qweb dependant, and need to be
- // able to map a qweb instance to a template name.
- this.id = nextID++;
- // slots contains sub templates defined with t-set inside t-component nodes, and
- // are meant to be used by the t-slot directive.
- this.slots = {};
- this.nextSlotId = 1;
- if (data) {
- this.addTemplates(data);
+ this.h = h;
+ // recursiveTemplates contains sub templates called with t-call, but which
+ // ends up in recursive situations. This is very similar to the slot situation,
+ // as in we need to propagate the scope.
+ this.recursiveFns = {};
+ this.isUpdating = false;
+ this.templates = Object.create(QWeb.TEMPLATES);
+ if (config.templates) {
+ this.addTemplates(config.templates);
+ }
+ if (config.translateFn) {
+ this.translateFn = config.translateFn;
}
}
static addDirective(directive) {
- DIRECTIVES.push(directive);
- DIRECTIVE_NAMES[directive.name] = 1;
- DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
+ if (directive.name in QWeb.DIRECTIVE_NAMES) {
+ throw new Error(`Directive "${directive.name} already registered`);
+ }
+ QWeb.DIRECTIVES.push(directive);
+ QWeb.DIRECTIVE_NAMES[directive.name] = 1;
+ QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
- directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
+ directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
}
}
- static register(name, Component) {
+ static registerComponent(name, Component) {
if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.components[name] = Component;
}
+ /**
+ * Register globally a template. All QWeb instances will obtain their
+ * templates from their own template map, and then, from the global static
+ * TEMPLATES property.
+ */
+ static registerTemplate(name, template) {
+ if (QWeb.TEMPLATES[name]) {
+ throw new Error(`Template '${name}' has already been registered`);
+ }
+ const qweb = new QWeb();
+ qweb.addTemplate(name, template);
+ QWeb.TEMPLATES[name] = qweb.templates[name];
+ }
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
@@ -1224,12 +1454,12 @@
this._addTemplate(name, doc.firstChild);
}
/**
- * Load templates from a xml (as a string). This will look up for the first
- * tag, and will consider each child of this as a template, with
- * the name given by the t-name attribute.
+ * Load templates from a xml (as a string or xml document). This will look up
+ * for the first tag, and will consider each child of this as a
+ * template, with the name given by the t-name attribute.
*/
addTemplates(xmlstr) {
- const doc = parseXML(xmlstr);
+ const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
@@ -1246,7 +1476,7 @@
this._processTemplate(elem);
const template = {
elem,
- fn: (context, extra) => {
+ fn: function (context, extra) {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
@@ -1300,44 +1530,82 @@
}
return template.fn.call(this, context, extra);
}
- _compile(name, elem, parentNode) {
+ /**
+ * Render a template to a html string.
+ *
+ * Note that this is more limited than the `render` method: it is not suitable
+ * to render a full component tree, since this is an asynchronous operation.
+ * This method can only render templates without components.
+ */
+ renderToString(name, context = {}, extra) {
+ const vnode = this.render(name, context, extra);
+ if (vnode.sel === undefined) {
+ return vnode.text;
+ }
+ const node = document.createElement(vnode.sel);
+ const result = patch(node, vnode);
+ return result.elm.outerHTML;
+ }
+ /**
+ * Force all widgets connected to this QWeb instance to rerender themselves.
+ *
+ * This method is mostly useful for external code that want to modify the
+ * application in some cases. For example, a router plugin.
+ */
+ forceUpdate() {
+ this.isUpdating = true;
+ Promise.resolve().then(() => {
+ if (this.isUpdating) {
+ this.isUpdating = false;
+ this.trigger("update");
+ }
+ });
+ }
+ _compile(name, elem, parentContext) {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
- const ctx = new Context(name);
- if (parentNode) {
- ctx.nextID = parentNode + 1;
- ctx.parentNode = parentNode;
+ const ctx = new CompilationContext(name);
+ if (elem.tagName !== "t") {
+ ctx.shouldDefineResult = false;
+ }
+ if (parentContext) {
+ ctx.templates = Object.create(parentContext.templates);
+ ctx.variables = Object.create(parentContext.variables);
+ ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true;
- ctx.addLine(`let c${parentNode} = extra.parentNode;`);
+ ctx.hasParentWidget = true;
+ ctx.shouldDefineResult = false;
+ ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
+ for (let v in parentContext.variables) {
+ let variable = parentContext.variables[v];
+ if (variable.id) {
+ ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
+ }
+ }
+ }
+ if (parentContext) {
+ ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
- if (ctx.shouldProtectContext) {
- ctx.code.unshift(" context = Object.create(context);");
- }
- if (ctx.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- ctx.code.unshift(" let owner = context;");
- }
- if (ctx.shouldDefineQWeb) {
- ctx.code.unshift(" let QWeb = this.constructor;");
- }
- if (ctx.shouldDefineUtils) {
- ctx.code.unshift(" let utils = this.utils;");
- }
- if (!parentNode) {
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
+ if (!parentContext) {
+ if (ctx.shouldDefineResult) {
+ ctx.addLine(`return result;`);
+ }
+ else {
+ if (!ctx.rootNode) {
+ throw new Error(`A template should have one root node (${ctx.templateName})`);
+ }
+ ctx.addLine(`return vn${ctx.rootNode};`);
}
- ctx.addLine(`return vn${ctx.rootNode};`);
}
+ let code = ctx.generateCode();
let template;
try {
- template = new Function("context", "extra", ctx.code.join("\n"));
+ template = new Function("context", "extra", code.join("\n"));
}
catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
- console.warn(ctx.code.join("\n"));
+ console.warn(code.join("\n"));
console.groupEnd();
throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`);
}
@@ -1364,8 +1632,18 @@
}
text = text.replace(whitespaceRE, " ");
}
+ if (this.translateFn) {
+ if (node.parentNode.getAttribute("t-translation") !== "off") {
+ text = this.translateFn(text);
+ }
+ }
if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
+ if (node.nodeType === 3) {
+ ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
+ }
+ else if (node.nodeType === 8) {
+ ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
+ }
}
else if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
@@ -1375,35 +1653,52 @@
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
+ ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
+ if (ctx !== ctx.rootContext) {
+ ctx = ctx.subContext("currentKey", ctx.currentKey);
+ }
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// to
node.setAttribute("t-component", node.tagName);
}
+ else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
+ throw new Error(`Directive 't-component' can only be used on nodes (used on a <${node.tagName}>)`);
+ }
const attributes = node.attributes;
const validDirectives = [];
- let withHandlers = false;
+ const finalizers = [];
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split(/-|\./)[0];
- if (!(dName in DIRECTIVE_NAMES)) {
+ if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
+ if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
+ const tNode = document.createElement("t");
+ tNode.setAttribute(attrName, node.getAttribute(attrName));
+ for (let child of Array.from(node.childNodes)) {
+ tNode.appendChild(child);
+ }
+ node.appendChild(tNode);
+ node.removeAttribute(attrName);
+ }
}
}
- const DIR_N = DIRECTIVES.length;
+ const DIR_N = QWeb.DIRECTIVES.length;
const ATTR_N = attributes.length;
+ let withHandlers = false;
for (let i = 0; i < DIR_N; i++) {
- let directive = DIRECTIVES[i];
+ let directive = QWeb.DIRECTIVES[i];
let fullName;
let value;
for (let j = 0; j < ATTR_N; j++) {
@@ -1421,6 +1716,9 @@
}
}
for (let { directive, value, fullName } of validDirectives) {
+ if (directive.finalize) {
+ finalizers.push({ directive, value, fullName });
+ }
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
@@ -1430,6 +1728,9 @@
value
});
if (isDone) {
+ for (let { directive, value, fullName } of finalizers) {
+ directive.finalize({ node, qweb: this, ctx, fullName, value });
+ }
return;
}
}
@@ -1471,10 +1772,18 @@
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.finalize) {
- directive.finalize({ node, qweb: this, ctx, fullName, value });
- }
+ // svg support
+ // we hadd svg namespace if it is a svg or if it is a g, but only if it is
+ // the root node. This is the easiest way to support svg sub components:
+ // they need to have a g tag as root. Otherwise, we would need a complete
+ // list of allowed svg tags.
+ const shouldAddNS = node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
+ if (shouldAddNS) {
+ ctx.rootContext.shouldDefineUtils = true;
+ ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
+ }
+ for (let { directive, value, fullName } of finalizers) {
+ directive.finalize({ node, qweb: this, ctx, fullName, value });
}
}
_compileGenericNode(node, ctx, withHandlers = true) {
@@ -1510,7 +1819,10 @@
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
- const value = attributes[i].textContent;
+ let value = attributes[i].textContent;
+ if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
+ value = this.translateFn(value);
+ }
// regular attributes
if (!name.startsWith("t-") && !node.getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
@@ -1537,9 +1849,10 @@
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
- let formattedValue = v.id || ctx.formatExpression(v);
+ let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
if (attName === "class") {
- formattedValue = `this.utils.toObj(${formattedValue})`;
+ ctx.rootContext.shouldDefineUtils = true;
+ formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
}
@@ -1594,13 +1907,7 @@
}
}
let nodeID = ctx.generateID();
- let nodeKey = node.getAttribute("t-key");
- if (nodeKey) {
- nodeKey = ctx.formatExpression(nodeKey);
- }
- else {
- nodeKey = nodeID;
- }
+ let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
@@ -1640,668 +1947,47 @@
}
}
}
+ QWeb.utils = UTILS;
QWeb.components = Object.create(null);
+ QWeb.DIRECTIVE_NAMES = {
+ name: 1,
+ att: 1,
+ attf: 1,
+ translation: 1
+ };
+ QWeb.DIRECTIVES = [];
+ QWeb.TEMPLATES = {};
+ QWeb.nextId = 1;
// dev mode enables better error messages or more costly validations
QWeb.dev = false;
- //------------------------------------------------------------------------------
- // Compilation Context
- //------------------------------------------------------------------------------
- class Context {
- constructor(name) {
- this.nextID = 1;
- this.code = [];
- this.variables = {};
- this.escaping = false;
- this.parentNode = null;
- this.parentTextNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldDefineQWeb = false;
- this.shouldDefineUtils = false;
- this.shouldProtectContext = false;
- this.inLoop = false;
- this.inPreTag = false;
- this.allowMultipleRoots = false;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.addLine("var h = this.utils.h;");
- }
- generateID() {
- const id = this.rootContext.nextID++;
- return id;
- }
- withParent(node) {
- if (!this.allowMultipleRoots &&
- this === this.rootContext &&
- (this.parentNode || this.parentTextNode)) {
- throw new Error("A template should not have more than one root node");
- }
- if (!this.rootContext.rootNode) {
- this.rootContext.rootNode = node;
- }
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- /**
- * Prepare an expression for being consumed at render time. Its main job
- * is to
- * - replace unknown variables by a lookup in the context
- * - replace already defined variables by their internal name
- */
- formatExpression(expr) {
- return compileExpr(expr, this.variables);
- }
- /**
- * Perform string interpolation on the given string. Note that if the whole
- * string is an expression, it simply returns it (formatted and enclosed in
- * parentheses).
- * For instance:
- * 'Hello {{x}}!' -> `Hello ${x}`
- * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
- */
- interpolate(s) {
- let matches = s.match(/\{\{.*?\}\}/g);
- if (matches && matches[0].length === s.length) {
- return `(${this.formatExpression(s.slice(2, -2))})`;
- }
- let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
- return "`" + r + "`";
- }
- }
+ // slots contains sub templates defined with t-set inside t-component nodes, and
+ // are meant to be used by the t-slot directive.
+ QWeb.slots = {};
+ QWeb.nextSlotId = 1;
- // If a component does not define explicitely a template
- // key, it needs to find a template with its name (or a parent's). This is
- // qweb dependant, so we need a place to store this information indexed by
- // qweb instances.
- const TEMPLATE_MAP = {};
- //------------------------------------------------------------------------------
- // Component
- //------------------------------------------------------------------------------
- let nextId = 1;
- class Component {
- //--------------------------------------------------------------------------
- // Lifecycle
- //--------------------------------------------------------------------------
- /**
- * Creates an instance of Component.
- *
- * The root component of a component tree needs an environment:
- *
- * ```javascript
- * const root = new RootComponent(env, props);
- * ```
- *
- * Every other component simply needs a reference to its parent:
- *
- * ```javascript
- * const child = new SomeComponent(parent, props);
- * ```
- *
- * Note that most of the time, only the root component needs to be created by
- * hand. Other components should be created automatically by the framework (with
- * the t-component directive in a template)
- */
- constructor(parent, props) {
- this.refs = {};
- const defaultProps = this.constructor.defaultProps;
- if (defaultProps) {
- props = this.__applyDefaultProps(props, defaultProps);
- }
- if (QWeb.dev) {
- this.__validateProps(props || {});
- }
- // is this a good idea?
- // Pro: if props is empty, we can create easily a component
- // Con: this is not really safe
- // Pro: but creating component (by a template) is always unsafe anyway
- this.props = props || {};
- let id = nextId++;
- let p = null;
- if (parent instanceof Component) {
- p = parent;
- this.env = parent.env;
- parent.__owl__.children[id] = this;
- }
- else {
- this.env = parent;
- this.env.qweb.on("update", this, () => {
- if (this.__owl__.isMounted) {
- this.render(true);
- }
- if (this.__owl__.isDestroyed) {
- // this is unlikely to happen, but if a root widget is destroyed,
- // we want to remove our subscription. The usual way to do that
- // would be to perform some check in the destroy method, but since
- // it is very performance sensitive, and since this is a rare event,
- // we simply do it lazily
- this.env.qweb.off("update", this);
- }
- });
- }
- this.__owl__ = {
- id: id,
- vnode: null,
- isMounted: false,
- isDestroyed: false,
- parent: p,
- children: {},
- cmap: {},
- renderId: 1,
- renderPromise: null,
- renderProps: props || null,
- boundHandlers: {},
- mountedHandlers: {}
- };
- }
- /**
- * The `el` is the root element of the component. Note that it could be null:
- * this is the case if the component is not mounted yet, or is destroyed.
- */
- get el() {
- return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
- }
- /**
- * willStart is an asynchronous hook that can be implemented to perform some
- * action before the initial rendering of a component.
- *
- * It will be called exactly once before the initial rendering. It is useful
- * in some cases, for example, to load external assets (such as a JS library)
- * before the component is rendered.
- *
- * Note that a slow willStart method will slow down the rendering of the user
- * interface. Therefore, some effort should be made to make this method as
- * fast as possible.
- *
- * Note: this method should not be called manually.
- */
- async willStart() { }
- /**
- * mounted is a hook that is called each time a component is attached to the
- * DOM. This is a good place to add some listeners, or to interact with the
- * DOM, if the component needs to perform some measure for example.
- *
- * Note: this method should not be called manually.
- *
- * @see willUnmount
- */
- mounted() { }
- /**
- * The willUpdateProps is an asynchronous hook, called just before new props
- * are set. This is useful if the component needs some asynchronous task
- * performed, depending on the props (for example, assuming that the props are
- * some record Id, fetching the record data).
- *
- * This hook is not called during the first render (but willStart is called
- * and performs a similar job).
- */
- async willUpdateProps(nextProps) { }
- /**
- * The willPatch hook is called just before the DOM patching process starts.
- * It is not called on the initial render. This is useful to get some
- * information which are in the DOM. For example, the current position of the
- * scrollbar
- *
- * The return value of willPatch will be given to the patched function.
- */
- willPatch() { }
- /**
- * This hook is called whenever a component did actually update its props,
- * state or env.
- *
- * This method is not called on the initial render. It is useful to interact
- * with the DOM (for example, through an external library) whenever the
- * component was updated.
- *
- * Updating the component state in this hook is possible, but not encouraged.
- * One need to be careful, because updates here will cause rerender, which in
- * turn will cause other calls to updated. So, we need to be particularly
- * careful at avoiding endless cycles.
- *
- * The snapshot parameter is the result of the call to willPatch.
- */
- patched(snapshot) { }
- /**
- * willUnmount is a hook that is called each time just before a component is
- * unmounted from the DOM. This is a good place to remove some listeners, for
- * example.
- *
- * Note: this method should not be called manually.
- *
- * @see mounted
- */
- willUnmount() { }
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
- /**
- * Mount the component to a target element.
- *
- * This should only be done if the component was created manually. Components
- * created declaratively in templates are managed by the Owl system.
- */
- async mount(target) {
- const vnode = await this.__prepare();
- if (this.__owl__.isDestroyed) {
- // component was destroyed before we get here...
- return;
- }
- this.__patch(vnode);
- target.appendChild(this.el);
- if (document.body.contains(target)) {
- this.__callMounted();
- }
- }
- unmount() {
- if (this.__owl__.isMounted) {
- this.__callWillUnmount();
- this.el.remove();
- }
- }
- async render(force = false, patchQueue) {
- const __owl__ = this.__owl__;
- if (!__owl__.isMounted) {
- return;
- }
- const shouldPatch = !patchQueue;
- if (shouldPatch) {
- patchQueue = [];
- }
- const renderVDom = this.__render(force, patchQueue);
- const renderId = __owl__.renderId;
- await renderVDom;
- if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
- // we only update the vnode and the actual DOM if no other rendering
- // occurred between now and when the render method was initially called.
- this.__applyPatchQueue(patchQueue);
- }
- }
- /**
- * Destroy the component. This operation is quite complex:
- * - it recursively destroy all children
- * - call the willUnmount hooks if necessary
- * - remove the dom node from the dom
- *
- * This should only be called manually if you created the component. Most
- * components will be automatically destroyed.
- */
- destroy() {
- const __owl__ = this.__owl__;
- if (!__owl__.isDestroyed) {
- const el = this.el;
- this.__destroy(__owl__.parent);
- if (el) {
- el.remove();
- }
- }
- }
- /**
- * This method is called by the component system whenever its props are
- * updated. If it returns true, then the component will be rendered.
- * Otherwise, it will skip the rendering (also, its props will not be updated)
- */
- shouldUpdate(nextProps) {
- return true;
- }
- /**
- * This method is the correct way to update the environment of a component. Doing
- * this will cause a full rerender of the component and its children, so this is
- * an operation that should not be done frequently.
- *
- * A good usecase for updating the environment would be to update some mostly
- * static config keys, such as a boolean to determine if we are in mobile
- * mode or not.
- */
- async updateEnv(nextEnv) {
- const __owl__ = this.__owl__;
- if (__owl__.parent && __owl__.parent.env === this.env) {
- this.env = Object.create(this.env);
- }
- Object.assign(this.env, nextEnv);
- if (__owl__.isMounted) {
- await this.render(true);
- }
- }
- /**
- * Emit a custom event of type 'eventType' with the given 'payload' on the
- * component's el, if it exists. However, note that the event will only bubble
- * up to the parent DOM nodes. Thus, it must be called between mounted() and
- * willUnmount().
- */
- trigger(eventType, payload) {
- if (this.el) {
- const ev = new CustomEvent(eventType, {
- bubbles: true,
- cancelable: true,
- detail: payload
- });
- this.el.dispatchEvent(ev);
- }
- }
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- __destroy(parent) {
- const __owl__ = this.__owl__;
- const isMounted = __owl__.isMounted;
- if (isMounted) {
- this.willUnmount();
- __owl__.isMounted = false;
- }
- const children = __owl__.children;
- for (let key in children) {
- children[key].__destroy(this);
- }
- if (parent) {
- let id = __owl__.id;
- delete parent.__owl__.children[id];
- __owl__.parent = null;
- }
- __owl__.isDestroyed = true;
- delete __owl__.vnode;
- }
- __callMounted() {
- const __owl__ = this.__owl__;
- const children = __owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (!comp.__owl__.isMounted && this.el.contains(comp.el)) {
- comp.__callMounted();
- }
- }
- __owl__.isMounted = true;
- const handlers = __owl__.mountedHandlers;
- for (let key in handlers) {
- handlers[key]();
- }
- this.mounted();
- }
- __callWillUnmount() {
- this.willUnmount();
- const __owl__ = this.__owl__;
- __owl__.isMounted = false;
- const children = __owl__.children;
- for (let id in children) {
- const comp = children[id];
- if (comp.__owl__.isMounted) {
- comp.__callWillUnmount();
- }
- }
- }
- async __updateProps(nextProps, forceUpdate = false, patchQueue) {
- const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
- if (shouldUpdate) {
- const defaultProps = this.constructor.defaultProps;
- if (defaultProps) {
- nextProps = this.__applyDefaultProps(nextProps, defaultProps);
- }
- if (QWeb.dev) {
- this.__validateProps(nextProps);
- }
- await this.willUpdateProps(nextProps);
- this.props = nextProps;
- await this.render(forceUpdate, patchQueue);
- }
- }
- __patch(vnode) {
- const __owl__ = this.__owl__;
- __owl__.renderPromise = null;
- const target = __owl__.vnode || document.createElement(vnode.sel);
- if (this.__owl__.classObj) {
- vnode.data.class = Object.assign(vnode.data.class || {}, this.__owl__.classObj);
- }
- __owl__.vnode = patch(target, vnode);
- }
- __prepare() {
- const __owl__ = this.__owl__;
- __owl__.renderProps = this.props;
- __owl__.renderPromise = this.__prepareAndRender();
- return __owl__.renderPromise;
- }
- async __prepareAndRender() {
- await this.willStart();
- const __owl__ = this.__owl__;
- if (__owl__.isDestroyed) {
- return Promise.resolve(h("div"));
- }
- const qweb = this.env.qweb;
- if (!this.template) {
- let tmap = TEMPLATE_MAP[qweb.id];
- if (!tmap) {
- tmap = {};
- TEMPLATE_MAP[qweb.id] = tmap;
- }
- let p = this.constructor;
- let name = p.name;
- let template = tmap[name];
- if (template) {
- this.template = template;
- }
- else {
- while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
- p = p.__proto__;
- }
- if (p === Component) {
- throw new Error(`Could not find template for component "${this.constructor.name}"`);
- }
- else {
- tmap[name] = template;
- this.template = template;
- }
- }
- }
- __owl__.render = qweb.render.bind(qweb, this.template);
- this.__observeState();
- return this.__render();
- }
- async __render(force = false, patchQueue = []) {
- const __owl__ = this.__owl__;
- __owl__.renderId++;
- const promises = [];
- const patch = [this];
- if (__owl__.isMounted) {
- patchQueue.push(patch);
- }
- if (__owl__.observer) {
- __owl__.observer.allowMutations = false;
- }
- let vnode = __owl__.render(this, {
- promises,
- handlers: __owl__.boundHandlers,
- mountedHandlers: __owl__.mountedHandlers,
- forceUpdate: force,
- patchQueue
- });
- patch.push(vnode);
- if (__owl__.observer) {
- __owl__.observer.allowMutations = true;
- }
- // this part is critical for the patching process to be done correctly. The
- // tricky part is that a child component can be rerendered on its own, which
- // will update its own vnode representation without the knowledge of the
- // parent component. With this, we make sure that the parent component will be
- // able to patch itself properly after
- vnode.key = __owl__.id;
- __owl__.renderProps = this.props;
- __owl__.renderPromise = Promise.all(promises).then(() => vnode);
- return __owl__.renderPromise;
- }
- /**
- * Only called by qweb t-component directive
- */
- __mount(vnode, elm) {
- const __owl__ = this.__owl__;
- if (__owl__.classObj) {
- vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
- }
- __owl__.vnode = patch(elm, vnode);
- if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) {
- this.__callMounted();
- }
- return __owl__.vnode;
- }
- /**
- * Only called by qweb t-component directive (when t-keepalive is set)
- */
- __remount() {
- const __owl__ = this.__owl__;
- if (!__owl__.isMounted) {
- __owl__.isMounted = true;
- this.mounted();
- }
- }
- __observeState() {
- if (this.state) {
- const __owl__ = this.__owl__;
- __owl__.observer = new Observer();
- __owl__.observer.observe(this.state);
- __owl__.observer.notifyCB = this.render.bind(this);
- }
- }
- /**
- * Apply default props (only top level).
- *
- * Note that this method does not modify in place the props, it returns a new
- * prop object
- */
- __applyDefaultProps(props, defaultProps) {
- props = props ? Object.create(props) : {};
- for (let propName in defaultProps) {
- if (props[propName] === undefined) {
- props[propName] = defaultProps[propName];
- }
- }
- return props;
- }
- /**
- * Apply the given patch queue. A patch is a pair [c, vn], where c is a
- * Component instance and vn a VNode.
- * 1) Call 'willPatch' on the component of each patch
- * 2) Call '__patch' on the component of each patch
- * 3) Call 'patched' on the component of each patch, in inverse order
- */
- __applyPatchQueue(patchQueue) {
- const patchLen = patchQueue.length;
- for (let i = 0; i < patchLen; i++) {
- const patch = patchQueue[i];
- patch.push(patch[0].willPatch());
- }
- for (let i = 0; i < patchLen; i++) {
- const patch = patchQueue[i];
- patch[0].__patch(patch[1]);
- }
- for (let i = patchLen - 1; i >= 0; i--) {
- const patch = patchQueue[i];
- patch[0].patched(patch[2]);
- }
- }
- /**
- * Validate the component props (or next props) against the (static) props
- * description. This is potentially an expensive operation: it may needs to
- * visit recursively the props and all the children to check if they are valid.
- * This is why it is only done in 'dev' mode.
- */
- __validateProps(props) {
- const propsDef = this.constructor.props;
- if (propsDef instanceof Array) {
- // list of strings (prop names)
- for (let i = 0, l = propsDef.length; i < l; i++) {
- if (!(propsDef[i] in props)) {
- throw new Error(`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`);
- }
- }
- }
- else if (propsDef) {
- // propsDef is an object now
- for (let propName in propsDef) {
- if (!(propName in props)) {
- if (propsDef[propName] && !propsDef[propName].optional) {
- throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`);
- }
- else {
- break;
- }
- }
- let isValid = isValidProp(props[propName], propsDef[propName]);
- if (!isValid) {
- throw new Error(`Props '${propName}' of invalid type in component '${this.constructor.name}'`);
- }
- }
- }
- }
- }
- //------------------------------------------------------------------------------
- // Prop validation helper
- //------------------------------------------------------------------------------
- /**
- * Check if an invidual prop value matches its (static) prop definition
- */
- function isValidProp(prop, propDef) {
- if (typeof propDef === "function") {
- // Check if a value is constructed by some Constructor. Note that there is a
- // slight abuse of language: we want to consider primitive values as well.
- //
- // So, even though 1 is not an instance of Number, we want to consider that
- // it is valid.
- if (typeof prop === "object") {
- return prop instanceof propDef;
- }
- return typeof prop === propDef.name.toLowerCase();
- }
- else if (propDef instanceof Array) {
- // If this code is executed, this means that we want to check if a prop
- // matches at least one of its descriptor.
- let result = false;
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
- result = result || isValidProp(prop, propDef[i]);
- }
- return result;
- }
- // propsDef is an object
- let result = isValidProp(prop, propDef.type);
- if (propDef.type === Array) {
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
- result = result && isValidProp(prop[i], propDef.element);
- }
- }
- if (propDef.type === Object) {
- const shape = propDef.shape;
- for (let key in shape) {
- result = result && isValidProp(prop[key], shape[key]);
- }
+ const parser = new DOMParser();
+ function htmlToVDOM(html) {
+ const doc = parser.parseFromString(html, "text/html");
+ const result = [];
+ for (let child of doc.body.childNodes) {
+ result.push(htmlToVNode(child));
}
return result;
}
+ function htmlToVNode(node) {
+ if (!(node instanceof Element)) {
+ return { text: node.textContent };
+ }
+ const attrs = {};
+ for (let attr of node.attributes) {
+ attrs[attr.name] = attr.textContent;
+ }
+ const children = [];
+ for (let c of node.childNodes) {
+ children.push(htmlToVNode(c));
+ }
+ return h(node.tagName, { attrs }, children);
+ }
/**
* Owl QWeb Directives
@@ -2319,17 +2005,21 @@
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
- UTILS.getFragment = function (str) {
+ QWeb.utils.getFragment = function (str) {
const temp = document.createElement("template");
temp.innerHTML = str;
return temp.content;
};
+ QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value, node, qweb, ctx) {
- if (value === "0" && ctx.caller) {
- qweb._compileNode(ctx.caller, ctx);
- return;
+ if (value === "0") {
+ const caller = ctx.getCaller();
+ if (caller) {
+ qweb._compileNode(caller, ctx.getInliningContext());
+ return;
+ }
}
- if (value.xml instanceof NodeList) {
+ if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(node, ctx);
}
@@ -2356,32 +2046,31 @@
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
+ if (ctx.rootContext.shouldDefineResult) {
+ ctx.addLine(`result = vn${nodeID}`);
+ }
}
}
else {
- let fragID = ctx.generateID();
- ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
- let tempNodeID = ctx.generateID();
- ctx.addLine(`var p${tempNodeID} = {hook: {`);
- ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
- ctx.addLine(`}};`);
- ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
- ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
+ ctx.rootContext.shouldDefineUtils = true;
+ ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
+ if (value.xml instanceof NodeList && value.id) {
+ ctx.addElse();
+ for (let node of Array.from(value.xml)) {
+ qweb._compileNode(node, ctx);
+ }
+ }
ctx.closeIf();
}
QWeb.addDirective({
name: "esc",
priority: 70,
atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
let value = ctx.getValue(node.getAttribute("t-esc"));
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
@@ -2391,10 +2080,6 @@
name: "raw",
priority: 80,
atNodeEncounter({ node, qweb, ctx }) {
- if (node.nodeName !== "t") {
- let nodeID = qweb._compileGenericNode(node, ctx);
- ctx = ctx.withParent(nodeID);
- }
let value = ctx.getValue(node.getAttribute("t-raw"));
compileValueNode(value, node, qweb, ctx);
return true;
@@ -2410,24 +2095,22 @@
atNodeEncounter({ node, ctx }) {
const variable = node.getAttribute("t-set");
let value = node.getAttribute("t-value");
+ ctx.variables[variable] = ctx.variables[variable] || {};
+ let qwebvar = ctx.variables[variable];
if (value) {
const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable)) {
- ctx.addLine(`${ctx.variables[variable].id} = ${formattedValue}`);
+ if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
+ ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
}
else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
- ctx.variables[variable] = {
- id: varName,
- expr: formattedValue
- };
+ qwebvar.id = varName;
+ qwebvar.expr = formattedValue;
}
}
else {
- ctx.variables[variable] = {
- xml: node.childNodes
- };
+ qwebvar.xml = node.childNodes;
}
return true;
}
@@ -2440,7 +2123,7 @@
priority: 20,
atNodeEncounter({ node, ctx }) {
let cond = ctx.getValue(node.getAttribute("t-if"));
- ctx.addIf(`${ctx.formatExpression(cond)}`);
+ ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id);
return false;
},
finalize({ ctx }) {
@@ -2452,7 +2135,7 @@
priority: 30,
atNodeEncounter({ node, ctx }) {
let cond = ctx.getValue(node.getAttribute("t-elif"));
- ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
+ ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
ctx.indent();
return false;
},
@@ -2490,13 +2173,50 @@
const nodeCopy = node.cloneNode(true);
nodeCopy.removeAttribute("t-call");
// extract variables from nodecopy
- const tempCtx = new Context();
- tempCtx.nextID = ctx.rootContext.nextID;
+ const tempCtx = new CompilationContext();
+ tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
- ctx.rootContext.nextID = tempCtx.nextID;
+ const templateMap = Object.create(ctx.templates);
// open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
+ // compile sub template
+ let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
+ subCtx = subCtx.subContext("templates", templateMap);
+ if (templateMap[subTemplate]) {
+ // OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
+ // This is a tricky situation... We obviously cannot inline the compiled
+ // template. So, what we need to do is to compile it, and make sure we
+ // properly transfer everything from the current scope to the sub template.
+ ctx.rootContext.shouldTrackScope = true;
+ ctx.rootContext.shouldDefineOwner = true;
+ let subTemplateName;
+ if (ctx.hasParentWidget) {
+ subTemplateName = ctx.templateName;
+ }
+ else {
+ subTemplateName = `__${ctx.generateID()}`;
+ subCtx.variables = {};
+ let id = 0;
+ for (let v in vars) {
+ subCtx.variables[v] = vars[v];
+ vars[v].id = `_v${id++}`;
+ }
+ const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
+ qweb.recursiveFns[subTemplateName] = subTemplateFn;
+ }
+ let varCode = `{}`;
+ if (Object.keys(vars).length) {
+ let id = 0;
+ const content = Object.values(vars)
+ .map((v) => `_v${id++}: ${v.expr}`)
+ .join(",");
+ varCode = `{${content}}`;
+ }
+ ctx.addLine(`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`);
+ return true;
+ }
+ templateMap[subTemplate] = true;
if (hasNewVariables) {
ctx.addLine("{");
ctx.indent();
@@ -2509,8 +2229,6 @@
// todo: handle XML variables...
}
}
- // compile sub template
- const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
@@ -2529,7 +2247,7 @@
priority: 10,
atNodeEncounter({ node, qweb, ctx }) {
ctx.rootContext.shouldProtectContext = true;
- ctx = ctx.subContext("inLoop", true);
+ ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach");
const name = node.getAttribute("t-as");
let arrayID = ctx.generateID();
@@ -2543,26 +2261,19 @@
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
- ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
+ const loopVar = `i${ctx.loopNumber}`;
+ ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
- ctx.addLine(`context.${name}_first = i === 0;`);
- ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
- ctx.addLine(`context.${name}_index = i;`);
- ctx.addLine(`context.${name} = _${keysID}[i];`);
- ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
+ ctx.addToScope(name + "_first", `${loopVar} === 0`);
+ ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
+ ctx.addToScope(name + "_index", loopVar);
+ ctx.addToScope(name, `_${keysID}[${loopVar}]`);
+ ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
const nodeCopy = node.cloneNode(true);
- let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
- if (!shouldWarn && node.tagName === "t") {
- if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
- shouldWarn = true;
- }
- if (!shouldWarn &&
- node.children.length === 1 &&
- node.children[0].tagName !== "t" &&
- !node.children[0].hasAttribute("t-key")) {
- shouldWarn = true;
- }
- }
+ let shouldWarn = !nodeCopy.hasAttribute("t-key") &&
+ node.children.length === 1 &&
+ node.children[0].tagName !== "t" &&
+ !node.children[0].hasAttribute("t-key");
if (shouldWarn) {
console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`);
}
@@ -2604,7 +2315,6 @@
* - t-on
* - t-ref
* - t-transition
- * - t-component/t-keepalive
* - t-mounted
* - t-slot
* - t-model
@@ -2633,30 +2343,33 @@
extraArgs = args.slice(1, -1);
return "";
});
- ctx.addIf(`!context['${handlerName}']`);
- ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`);
- ctx.closeIf();
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
- let handler;
- if (mods.length > 0) {
- handler = `function (e) {`;
- handler += mods
- .map(function (mod) {
- return MODS_CODE[mod];
- })
- .join("");
- handler += `context['${handlerName}'].call(${params}, e);}`;
+ let handler = `function (e) {`;
+ handler += mods
+ .map(function (mod) {
+ return MODS_CODE[mod];
+ })
+ .join("");
+ if (handlerName) {
+ if (!extraArgs) {
+ handler += `const fn = context['${handlerName}'];`;
+ handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
+ handler += `}`;
+ ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`);
+ ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
+ }
+ else {
+ const handlerKey = `handler${ctx.generateID()}`;
+ ctx.addLine(`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`);
+ handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
+ handler += `}`;
+ ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
+ }
}
else {
- handler = `context['${handlerName}'].bind(${params})`;
- }
- if (extraArgs) {
+ handler += "}";
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
}
- else {
- ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`);
- ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
- }
}
});
//------------------------------------------------------------------------------
@@ -2666,18 +2379,20 @@
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
+ ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
- addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
+ addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
+ addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
}
});
//------------------------------------------------------------------------------
// t-transition
//------------------------------------------------------------------------------
- UTILS.nextFrame = function (cb) {
+ QWeb.utils.nextFrame = function (cb) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
- UTILS.transitionInsert = function (vn, name) {
+ QWeb.utils.transitionInsert = function (vn, name) {
const elm = vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
@@ -2687,6 +2402,8 @@
}
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
+ elm.classList.remove(name + "-leave-active");
+ elm.classList.remove(name + "-leave-to");
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
@@ -2697,12 +2414,15 @@
whenTransitionEnd(elm, finalize);
});
};
- UTILS.transitionRemove = function (vn, name, rm) {
+ QWeb.utils.transitionRemove = function (vn, name, rm) {
const elm = vn.elm;
elm.setAttribute("data-owl-key", vn.key);
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
const finalize = () => {
+ if (!elm.classList.contains(name + "-leave-active")) {
+ return;
+ }
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-leave-to");
rm();
@@ -2744,23 +2464,158 @@
QWeb.addDirective({
name: "transition",
priority: 96,
- atNodeCreation({ value, addNodeHook }) {
+ atNodeCreation({ ctx, value, addNodeHook }) {
+ ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
- insert: `this.utils.transitionInsert(vn, '${name}');`,
- remove: `this.utils.transitionRemove(vn, '${name}', rm);`
+ insert: `utils.transitionInsert(vn, '${name}');`,
+ remove: `utils.transitionRemove(vn, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
+ //------------------------------------------------------------------------------
+ // t-slot
+ //------------------------------------------------------------------------------
+ QWeb.addDirective({
+ name: "slot",
+ priority: 80,
+ atNodeEncounter({ ctx, value }) {
+ const slotKey = ctx.generateID();
+ ctx.rootContext.shouldDefineOwner = true;
+ ctx.addLine(`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`);
+ ctx.addIf(`slot${slotKey}`);
+ let parentNode = `c${ctx.parentNode}`;
+ if (!ctx.parentNode) {
+ ctx.rootContext.shouldDefineResult = true;
+ ctx.rootContext.shouldDefineUtils = true;
+ parentNode = `children${ctx.generateID()}`;
+ ctx.addLine(`let ${parentNode}= []`);
+ ctx.addLine(`result = {}`);
+ }
+ ctx.addLine(`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`);
+ if (!ctx.parentNode) {
+ ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
+ }
+ ctx.closeIf();
+ return true;
+ }
+ });
+ //------------------------------------------------------------------------------
+ // t-model
+ //------------------------------------------------------------------------------
+ QWeb.utils.toNumber = function (val) {
+ const n = parseFloat(val);
+ return isNaN(n) ? val : n;
+ };
+ QWeb.addDirective({
+ name: "model",
+ priority: 42,
+ atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
+ const type = node.getAttribute("type");
+ let handler;
+ let event = fullName.includes(".lazy") ? "change" : "input";
+ // we keep here a reference to the "base expression" (if the expression
+ // is `t-model="some.expr.value", then the base expression is "some.expr").
+ // This is necessary so we can capture it in the handler closure.
+ let expr = ctx.formatExpression(value);
+ const index = expr.lastIndexOf(".");
+ const baseExpr = expr.slice(0, index);
+ ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
+ expr = `expr${nodeID}.${expr.slice(index + 1)}`;
+ const key = ctx.generateTemplateKey();
+ if (node.tagName === "select") {
+ ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
+ addNodeHook("create", `n.elm.value=${expr};`);
+ event = "change";
+ handler = `(ev) => {${expr} = ev.target.value}`;
+ }
+ else if (type === "checkbox") {
+ ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
+ handler = `(ev) => {${expr} = ev.target.checked}`;
+ }
+ else if (type === "radio") {
+ const nodeValue = node.getAttribute("value");
+ ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
+ handler = `(ev) => {${expr} = ev.target.value}`;
+ event = "click";
+ }
+ else {
+ ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
+ const trimCode = fullName.includes(".trim") ? ".trim()" : "";
+ let valueCode = `ev.target.value${trimCode}`;
+ if (fullName.includes(".number")) {
+ ctx.rootContext.shouldDefineUtils = true;
+ valueCode = `utils.toNumber(${valueCode})`;
+ }
+ handler = `(ev) => {${expr} = ${valueCode}}`;
+ }
+ ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
+ ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
+ }
+ });
+ //------------------------------------------------------------------------------
+ // t-key
+ //------------------------------------------------------------------------------
+ QWeb.addDirective({
+ name: "key",
+ priority: 45,
+ atNodeEncounter({ ctx, value }) {
+ let id = ctx.generateID();
+ ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
+ ctx.currentKey = `nodeKey${id}`;
+ }
+ });
+
+ const config = {};
+ Object.defineProperty(config, "mode", {
+ get() {
+ return QWeb.dev ? "dev" : "prod";
+ },
+ set(mode) {
+ QWeb.dev = mode === "dev";
+ if (QWeb.dev) {
+ const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
+ console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
+ }
+ else {
+ console.log(`Owl is now running in 'prod' mode.`);
+ }
+ }
+ });
+
+ /**
+ * We define here OwlEvent, a subclass of CustomEvent, with an additional
+ * attribute:
+ * - originalComponent: the component that triggered the event
+ */
+ class OwlEvent extends CustomEvent {
+ constructor(component, eventType, options) {
+ super(eventType, options);
+ this.originalComponent = component;
+ }
+ }
+
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
+ QWeb.utils.defineProxy = function defineProxy(target, source) {
+ for (let k in source) {
+ Object.defineProperty(target, k, {
+ get() {
+ return source[k];
+ },
+ set(val) {
+ source[k] = val;
+ }
+ });
+ }
+ };
/**
* The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
@@ -2923,15 +2778,15 @@
*/
QWeb.addDirective({
name: "component",
- extraNames: ["props", "keepalive", "asyncroot"],
+ extraNames: ["props"],
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }) {
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
+ ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
- let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- let async = node.getAttribute("t-asyncroot") ? true : false;
+ let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
const events = [];
let transition = "";
@@ -2943,11 +2798,11 @@
if (name.startsWith("t-on-")) {
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
- let handlerName = value.replace(/\(.*\)/, function (args) {
+ let handlerValue = value.replace(/\(.*\)/, function (args) {
extraArgs = args.slice(1, -1);
return "";
});
- events.push([eventName, mods, handlerName, extraArgs]);
+ events.push([eventName, mods, handlerValue, extraArgs]);
}
else if (name === "t-transition") {
transition = value;
@@ -2959,60 +2814,30 @@
}
}
}
- let key = node.getAttribute("t-key");
- if (key) {
- key = ctx.formatExpression(key);
- }
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
- let dummyID = ctx.generateID();
- let defID = ctx.generateID();
let componentID = ctx.generateID();
- let keyID = key && ctx.generateID();
- if (key) {
- // we bind a variable to the key (could be a complex expression, so we
- // want to evaluate it only once)
- ctx.addLine(`let key${keyID} = ${key};`);
- }
- ctx.addLine(`let def${defID};`);
- let templateID = key
- ? `key${keyID}`
- : ctx.inLoop
- ? `String(-${componentID} - i)`
- : String(componentID);
- if (ctx.allowMultipleRoots) {
- // necessary to prevent collisions
- if (!key && ctx.inLoop) {
- let id = ctx.generateID();
- ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
- templateID = `template${id}`;
- }
- else {
- templateID = `"_slot_${templateID}"`;
- }
- }
+ const templateKey = ctx.generateTemplateKey();
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey = "";
if (ref) {
+ ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
- refExpr = `context.refs[${refKey}] = w${componentID};`;
+ refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
- let transitionsInsertCode = "";
- if (transition) {
- transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
- }
- let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
- if (ref && !keepAlive) {
- finalizeComponentCode += `delete context.refs[${refKey}];`;
+ let finalizeComponentCode = `w${componentID}.destroy();`;
+ if (ref) {
+ finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
${finalizeComponentCode}
};
+ delete w${componentID}.__owl__.transitionInserted;
utils.transitionRemove(vn, '${transition}', finalize);`;
}
let createHook = "";
@@ -3039,7 +2864,7 @@
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
- tattExpr = `this.utils.toObj(${tattExpr})`;
+ tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -3050,10 +2875,10 @@
}
}
let eventsCode = events
- .map(function ([eventName, mods, handlerName, extraArgs]) {
+ .map(function ([eventName, mods, handlerValue, extraArgs]) {
let params = "owner";
if (extraArgs) {
- if (ctx.inLoop) {
+ if (ctx.loopNumber) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
@@ -3065,19 +2890,17 @@
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
- let handler;
- if (mods.length > 0) {
- handler = `function (e) {`;
- handler += mods
- .map(function (mod) {
- return T_COMPONENT_MODS_CODE[mod];
- })
- .join("");
- handler += `owner['${handlerName}'].call(${params}, e);}`;
- }
- else {
- handler = `owner['${handlerName}'].bind(${params})`;
+ let handler = `function (e) {`;
+ handler += mods
+ .map(function (mod) {
+ return T_COMPONENT_MODS_CODE[mod];
+ })
+ .join("");
+ if (handlerValue) {
+ handler += `const fn = owner['${handlerValue}'];`;
+ handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
}
+ handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
@@ -3085,37 +2908,87 @@
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
- ctx.addLine(`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`);
- ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
- if (async) {
- ctx.addLine(`const patchQueue${componentID} = [];`);
- ctx.addLine(`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`);
+ ctx.addLine(`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`);
+ let shouldProxy = !ctx.parentNode;
+ if (shouldProxy) {
+ let id = ctx.generateID();
+ ctx.rootContext.rootNode = id;
+ shouldProxy = true;
+ ctx.rootContext.shouldDefineResult = true;
+ ctx.addLine(`let vn${id} = {};`);
+ ctx.addLine(`result = vn${id};`);
+ }
+ if (hasDynamicProps) {
+ const dynamicProp = ctx.formatExpression(node.getAttribute("t-props"));
+ ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
}
else {
- ctx.addLine(`c${ctx.parentNode}.push(null);`);
+ ctx.addLine(`let props${componentID} = {${propStr}};`);
}
- ctx.addLine(`let props${componentID} = {${propStr}};`);
- ctx.addIf(`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`);
- ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
- ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
- ctx.addElse();
+ ctx.addIf(`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`);
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
ctx.closeIf();
- ctx.closeIf();
- ctx.addIf(`!w${componentID}`);
+ let registerCode = "";
+ if (shouldProxy) {
+ registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
+ }
+ // SLOTS
+ const varDefs = [];
+ const hasSlots = node.childNodes.length;
+ if (hasSlots) {
+ ctx.rootContext.shouldTrackScope = true;
+ for (let v of Object.values(ctx.variables)) {
+ if (v["id"]) {
+ varDefs.push(v["id"]);
+ }
+ }
+ }
+ let scopeVars;
+ if (hasSlots) {
+ let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
+ let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
+ scopeVars = `${scope}, ${vars}`;
+ }
+ else {
+ scopeVars = "undefined, undefined";
+ }
+ ctx.addIf(`w${componentID}`);
+ // need to update component
+ let styleCode = "";
+ if (tattStyle) {
+ styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
+ }
+ ctx.addLine(`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
+ ", " + scopeVars})${styleCode};`);
+ ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
+ if (registerCode) {
+ ctx.addLine(registerCode);
+ }
+ if (ctx.parentNode) {
+ ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
+ }
+ ctx.addElse();
// new component
- ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
- ctx.addLine(`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`);
+ let dynamicFallback = "";
+ if (!value.match(INTERP_REGEXP)) {
+ dynamicFallback = `|| ${ctx.formatExpression(value)}`;
+ }
+ const interpValue = ctx.interpolate(value);
+ ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
+ ctx.addLine(`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`);
// maybe only do this in dev mode...
ctx.addLine(`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`);
- ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`);
- ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
- // SLOTS
- if (node.childNodes.length) {
+ ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
+ if (transition) {
+ ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
+ ctx.addLine(`w${componentID}.__patch = fiber => {__patch${componentID}.call(w${componentID}, fiber); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`);
+ }
+ ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
+ if (hasSlots) {
const clone = node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
- const slotId = qweb.nextSlotId++;
+ const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) {
@@ -3123,8 +2996,8 @@
slotNode.parentElement.removeChild(slotNode);
const key = slotNode.getAttribute("t-set");
slotNode.removeAttribute("t-set");
- const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode);
- qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
+ const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
+ QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
@@ -3132,450 +3005,1822 @@
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
- const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode);
- qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
+ const slotFn = qweb._compile(`slot_default_template`, t, ctx);
+ QWeb.slots[`${slotId}_default`] = slotFn;
}
}
- ctx.addLine(`def${defID} = w${componentID}.__prepare();`);
+ ctx.addLine(`let fiber = w${componentID}.__prepare(extra.fiber, ${scopeVars}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`);
- ctx.addElse();
- // need to update component
- const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
- ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`);
- let keepAliveCode = "";
- if (keepAlive) {
- keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
+ const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
+ ctx.addLine(`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`);
+ if (registerCode) {
+ ctx.addLine(registerCode);
}
- ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`);
+ if (ctx.parentNode) {
+ ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
+ }
+ ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
- if (async) {
- ctx.addLine(`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`);
- }
- else {
- ctx.addLine(`extra.promises.push(def${defID});`);
- }
- if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
- ctx.closeIf();
- }
+ ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
return true;
}
});
- //------------------------------------------------------------------------------
- // t-mounted
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "mounted",
- priority: 97,
- atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
- ctx.rootContext.shouldDefineOwner = true;
- const eventName = fullName.slice(5);
- if (!eventName) {
- throw new Error("Missing event name with t-on directive");
- }
- let extraArgs;
- let handler = value.replace(/\(.*\)/, function (args) {
- extraArgs = args.slice(1, -1);
- return "";
- });
- let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`;
- if (extraArgs) {
- ctx.addLine(`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`);
- }
- else {
- ctx.addLine(`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`);
- }
- addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
- }
- });
- //------------------------------------------------------------------------------
- // t-slot
- //------------------------------------------------------------------------------
- QWeb.addDirective({
- name: "slot",
- priority: 80,
- atNodeEncounter({ ctx, value }) {
- const slotKey = ctx.generateID();
- ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
- ctx.addIf(`slot${slotKey}`);
- ctx.addLine(`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`);
- ctx.closeIf();
- return true;
- }
- });
- //------------------------------------------------------------------------------
- // t-model
- //------------------------------------------------------------------------------
- UTILS.toNumber = function (val) {
- const n = parseFloat(val);
- return isNaN(n) ? val : n;
- };
- QWeb.addDirective({
- name: "model",
- priority: 42,
- atNodeCreation({ ctx, nodeID, value, node, fullName }) {
- const type = node.getAttribute("type");
- let handler;
- let event = fullName.includes(".lazy") ? "change" : "input";
- if (node.tagName === "select") {
- ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
- event = "change";
- handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
- }
- else if (type === "checkbox") {
- ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
- handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
- }
- else if (type === "radio") {
- const nodeValue = node.getAttribute("value");
- ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
- handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
- event = "click";
- }
- else {
- ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
- const trimCode = fullName.includes(".trim") ? ".trim()" : "";
- let valueCode = `ev.target.value${trimCode}`;
- if (fullName.includes(".number")) {
- ctx.rootContext.shouldDefineUtils = true;
- valueCode = `utils.toNumber(${valueCode})`;
- }
- handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
- }
- ctx.addLine(`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`);
- ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
- }
- });
- class Store extends EventBus {
- constructor(config, options = {}) {
- super();
- this._commitLevel = 0;
- this.history = [];
- this._updateId = 1;
- this.debug = options.debug || false;
- this.state = config.state || {};
- this.actions = config.actions;
- this.mutations = config.mutations;
- this.env = config.env;
- this.observer = new Observer();
- this.observer.notifyCB = this.__notifyComponents.bind(this);
- this.observer.allowMutations = false;
- this.observer.observe(this.state);
- this.getters = {};
- this._gettersCache = {};
- if (this.debug) {
- this.history.push({ state: this.state });
- }
- const cTypes = ["undefined", "number", "string"];
- for (let entry of Object.entries(config.getters || {})) {
- const name = entry[0];
- const func = entry[1];
- this.getters[name] = payload => {
- if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
- this._gettersCache[name] = this._gettersCache[name] || {};
- this._gettersCache[name][payload] =
- this._gettersCache[name][payload] ||
- func({ state: this.state, getters: this.getters }, payload);
- return this._gettersCache[name][payload];
+ class Scheduler {
+ constructor(requestAnimationFrame) {
+ this.tasks = [];
+ this.isRunning = false;
+ this.requestAnimationFrame = requestAnimationFrame;
+ }
+ start() {
+ this.isRunning = true;
+ this.scheduleTasks();
+ }
+ stop() {
+ this.isRunning = false;
+ }
+ addFiber(fiber) {
+ // if the fiber was remapped into a larger rendering fiber, it may not be a
+ // root fiber. But we only want to register root fibers
+ fiber = fiber.root;
+ return new Promise((resolve, reject) => {
+ if (fiber.error) {
+ return reject(fiber.error);
+ }
+ this.tasks.push({
+ fiber,
+ callback: () => {
+ if (fiber.error) {
+ return reject(fiber.error);
+ }
+ resolve();
}
- return func({ state: this.state, getters: this.getters }, payload);
+ });
+ if (!this.isRunning) {
+ this.start();
+ }
+ });
+ }
+ /**
+ * Process all current tasks. This only applies to the fibers that are ready.
+ * Other tasks are left unchanged.
+ */
+ flush() {
+ let tasks = this.tasks;
+ this.tasks = [];
+ tasks = tasks.filter(task => {
+ if (task.fiber.isCompleted) {
+ task.callback();
+ return false;
+ }
+ if (task.fiber.counter === 0) {
+ if (!task.fiber.error) {
+ try {
+ task.fiber.complete();
+ }
+ catch (e) {
+ task.fiber.handleError(e);
+ }
+ }
+ task.callback();
+ return false;
+ }
+ return true;
+ });
+ this.tasks = tasks.concat(this.tasks);
+ if (this.tasks.length === 0) {
+ this.stop();
+ }
+ }
+ scheduleTasks() {
+ this.requestAnimationFrame(() => {
+ this.flush();
+ if (this.isRunning) {
+ this.scheduleTasks();
+ }
+ });
+ }
+ }
+ const raf = window.requestAnimationFrame.bind(window);
+ const scheduler = new Scheduler(raf);
+
+ /**
+ * Owl Fiber Class
+ *
+ * Fibers are small abstractions designed to contain all the internal state
+ * associated with a "rendering work unit", relative to a specific component.
+ *
+ * A rendering will cause the creation of a fiber for each impacted components.
+ *
+ * Fibers capture all that necessary information, which is critical to owl
+ * asynchronous rendering pipeline. Fibers can be cancelled, can be in different
+ * states and in general determine the state of the rendering.
+ */
+ class Fiber {
+ constructor(parent, component, force, target) {
+ this.id = Fiber.nextId++;
+ // isCompleted means that the rendering corresponding to this fiber's work is
+ // done, either because the component has been mounted or patched, or because
+ // fiber has been cancelled.
+ this.isCompleted = false;
+ // the fibers corresponding to component updates (updateProps) need to call
+ // the willPatch and patched hooks from the corresponding component. However,
+ // fibers corresponding to a new component do not need to do that. So, the
+ // shouldPatch hook is the boolean that we check whenever we need to apply
+ // a patch.
+ this.shouldPatch = true;
+ // isRendered is the last state of a fiber. If true, this means that it has
+ // been rendered and is inert (so, it should not be taken into account when
+ // counting the number of active fibers).
+ this.isRendered = false;
+ // the counter number is a critical information. It is only necessary for a
+ // root fiber. For that fiber, this number counts the number of active sub
+ // fibers. When that number reaches 0, the fiber can be applied by the
+ // scheduler.
+ this.counter = 0;
+ this.vnode = null;
+ this.child = null;
+ this.sibling = null;
+ this.lastChild = null;
+ this.parent = null;
+ this.component = component;
+ this.force = force;
+ this.target = target;
+ const __owl__ = component.__owl__;
+ this.scope = __owl__.scope;
+ this.vars = __owl__.vars;
+ this.root = parent ? parent.root : this;
+ this.parent = parent;
+ let oldFiber = __owl__.currentFiber;
+ if (oldFiber && !oldFiber.isCompleted) {
+ if (oldFiber.root === oldFiber && !parent) {
+ // both oldFiber and this fiber are root fibers
+ this._reuseFiber(oldFiber);
+ return oldFiber;
+ }
+ else {
+ this._remapFiber(oldFiber);
+ }
+ }
+ this.root.counter++;
+ __owl__.currentFiber = this;
+ }
+ /**
+ * When the oldFiber is not completed yet, and both oldFiber and this fiber
+ * are root fibers, we want to reuse the oldFiber instead of creating a new
+ * one. Doing so will guarantee that the initiator(s) of those renderings will
+ * be notified (the promise will resolve) when the last rendering will be done.
+ *
+ * This function thus assumes that oldFiber is a root fiber.
+ */
+ _reuseFiber(oldFiber) {
+ oldFiber.cancel(); // cancel children fibers
+ oldFiber.isCompleted = false; // keep the root fiber alive
+ oldFiber.isRendered = false; // the fiber has to be re-rendered
+ if (oldFiber.child) {
+ // remove relation to children
+ oldFiber.child.parent = null;
+ oldFiber.child = null;
+ oldFiber.lastChild = null;
+ }
+ oldFiber.counter = 1; // re-initialize counter
+ oldFiber.id = Fiber.nextId++;
+ }
+ /**
+ * In some cases, a rendering initiated at some component can detect that it
+ * should be part of a larger rendering initiated somewhere up the component
+ * tree. In that case, it needs to cancel the previous rendering and
+ * remap itself as a part of the current parent rendering.
+ */
+ _remapFiber(oldFiber) {
+ oldFiber.cancel();
+ this.shouldPatch = oldFiber.shouldPatch;
+ if (oldFiber === oldFiber.root) {
+ oldFiber.counter++;
+ }
+ if (oldFiber.parent && !this.parent) {
+ // re-map links
+ this.parent = oldFiber.parent;
+ this.root = this.parent.root;
+ this.sibling = oldFiber.sibling;
+ if (this.parent.lastChild === oldFiber) {
+ this.parent.lastChild = this;
+ }
+ if (this.parent.child === oldFiber) {
+ this.parent.child = this;
+ }
+ else {
+ let current = this.parent.child;
+ while (true) {
+ if (current.sibling === oldFiber) {
+ current.sibling = this;
+ break;
+ }
+ current = current.sibling;
+ }
+ }
+ }
+ }
+ /**
+ * This function has been taken from
+ * https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
+ */
+ _walk(doWork) {
+ let root = this;
+ let current = this;
+ while (true) {
+ const child = doWork(current);
+ if (child) {
+ current = child;
+ continue;
+ }
+ if (current === root) {
+ return;
+ }
+ while (!current.sibling) {
+ if (!current.parent || current.parent === root) {
+ return;
+ }
+ current = current.parent;
+ }
+ current = current.sibling;
+ }
+ }
+ /**
+ * Successfully complete the work of the fiber: call the mount or patch hooks
+ * and patch the DOM. This function is called once the fiber and its children
+ * are ready, and the scheduler decides to process it.
+ */
+ complete() {
+ let component = this.component;
+ this.isCompleted = true;
+ if (!this.target && !component.__owl__.isMounted) {
+ return;
+ }
+ // build patchQueue
+ const patchQueue = [];
+ const doWork = function (f) {
+ patchQueue.push(f);
+ return f.child;
+ };
+ this._walk(doWork);
+ const patchLen = patchQueue.length;
+ // call willPatch hook on each fiber of patchQueue
+ for (let i = 0; i < patchLen; i++) {
+ const fiber = patchQueue[i];
+ if (fiber.shouldPatch) {
+ component = fiber.component;
+ if (component.__owl__.willPatchCB) {
+ component.__owl__.willPatchCB();
+ }
+ component.willPatch();
+ }
+ }
+ // call __patch on each fiber of (reversed) patchQueue
+ for (let i = patchLen - 1; i >= 0; i--) {
+ const fiber = patchQueue[i];
+ component = fiber.component;
+ component.__patch(fiber.vnode);
+ if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
+ component.__owl__.pvnode.elm = component.__owl__.vnode.elm;
+ }
+ component.__owl__.currentFiber = null;
+ }
+ // insert into the DOM (mount case)
+ let inDOM = false;
+ if (this.target) {
+ this.target.appendChild(this.component.el);
+ inDOM = document.body.contains(this.target);
+ this.component.env.qweb.trigger("dom-appended");
+ }
+ // call patched/mounted hook on each fiber of (reversed) patchQueue
+ for (let i = patchLen - 1; i >= 0; i--) {
+ const fiber = patchQueue[i];
+ component = fiber.component;
+ if (fiber.shouldPatch && !this.target) {
+ component.patched();
+ if (component.__owl__.patchedCB) {
+ component.__owl__.patchedCB();
+ }
+ }
+ else if (this.target ? inDOM : true) {
+ component.__callMounted();
+ }
+ }
+ }
+ /**
+ * Cancel a fiber and all its children.
+ */
+ cancel() {
+ this._walk(f => {
+ if (!f.isRendered) {
+ f.root.counter--;
+ }
+ f.isCompleted = true;
+ return f.child;
+ });
+ }
+ /**
+ * This is the global error handler for errors occurring in Owl main lifecycle
+ * methods. Caught errors are triggered on the QWeb instance, and are
+ * potentially given to some parent component which implements `catchError`.
+ *
+ * If there are no such component, we destroy everything. This is better than
+ * being in a corrupted state.
+ */
+ handleError(error) {
+ let component = this.component;
+ this.vnode = component.__owl__.vnode || h("div");
+ const qweb = component.env.qweb;
+ let root = component;
+ let canCatch = false;
+ while (component && !(canCatch = !!component.catchError)) {
+ root = component;
+ component = component.__owl__.parent;
+ }
+ qweb.trigger("error", error);
+ if (canCatch) {
+ component.catchError(error);
+ }
+ else {
+ // the 3 next lines aim to mark the root fiber as being in error, and
+ // to force it to end, without waiting for its children
+ this.root.counter = 0;
+ this.root.error = error;
+ scheduler.flush();
+ root.destroy();
+ }
+ }
+ }
+ Fiber.nextId = 1;
+
+ //------------------------------------------------------------------------------
+ // Prop validation helper
+ //------------------------------------------------------------------------------
+ /**
+ * Validate the component props (or next props) against the (static) props
+ * description. This is potentially an expensive operation: it may needs to
+ * visit recursively the props and all the children to check if they are valid.
+ * This is why it is only done in 'dev' mode.
+ */
+ QWeb.utils.validateProps = function (Widget, props) {
+ const propsDef = Widget.props;
+ if (propsDef instanceof Array) {
+ // list of strings (prop names)
+ for (let i = 0, l = propsDef.length; i < l; i++) {
+ const propName = propsDef[i];
+ if (propName[propName.length - 1] === "?") {
+ // optional prop
+ break;
+ }
+ if (!(propName in props)) {
+ throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
+ }
+ }
+ for (let key in props) {
+ if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
+ throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
+ }
+ }
+ }
+ else if (propsDef) {
+ // propsDef is an object now
+ for (let propName in propsDef) {
+ if (props[propName] === undefined) {
+ if (propsDef[propName] && !propsDef[propName].optional) {
+ throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
+ }
+ else {
+ break;
+ }
+ }
+ let isValid;
+ try {
+ isValid = isValidProp(props[propName], propsDef[propName]);
+ }
+ catch (e) {
+ e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
+ throw e;
+ }
+ if (!isValid) {
+ throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
+ }
+ }
+ for (let propName in props) {
+ if (!(propName in propsDef)) {
+ throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
+ }
+ }
+ }
+ };
+ /**
+ * Check if an invidual prop value matches its (static) prop definition
+ */
+ function isValidProp(prop, propDef) {
+ if (propDef === true) {
+ return true;
+ }
+ if (typeof propDef === "function") {
+ // Check if a value is constructed by some Constructor. Note that there is a
+ // slight abuse of language: we want to consider primitive values as well.
+ //
+ // So, even though 1 is not an instance of Number, we want to consider that
+ // it is valid.
+ if (typeof prop === "object") {
+ return prop instanceof propDef;
+ }
+ return typeof prop === propDef.name.toLowerCase();
+ }
+ else if (propDef instanceof Array) {
+ // If this code is executed, this means that we want to check if a prop
+ // matches at least one of its descriptor.
+ let result = false;
+ for (let i = 0, iLen = propDef.length; i < iLen; i++) {
+ result = result || isValidProp(prop, propDef[i]);
+ }
+ return result;
+ }
+ // propsDef is an object
+ if (propDef.optional && prop === undefined) {
+ return true;
+ }
+ let result = propDef.type ? isValidProp(prop, propDef.type) : true;
+ if (propDef.validate) {
+ result = result && propDef.validate(prop);
+ }
+ if (propDef.type === Array && propDef.element) {
+ for (let i = 0, iLen = prop.length; i < iLen; i++) {
+ result = result && isValidProp(prop[i], propDef.element);
+ }
+ }
+ if (propDef.type === Object && propDef.shape) {
+ const shape = propDef.shape;
+ for (let key in shape) {
+ result = result && isValidProp(prop[key], shape[key]);
+ }
+ if (result) {
+ for (let propName in prop) {
+ if (!(propName in shape)) {
+ throw new Error(`unknown prop '${propName}'`);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ const portalSymbol = Symbol("portal"); // FIXME
+ //------------------------------------------------------------------------------
+ // Component
+ //------------------------------------------------------------------------------
+ let nextId = 1;
+ class Component {
+ //--------------------------------------------------------------------------
+ // Lifecycle
+ //--------------------------------------------------------------------------
+ /**
+ * Creates an instance of Component.
+ *
+ * Note that most of the time, only the root component needs to be created by
+ * hand. Other components should be created automatically by the framework (with
+ * the t-component directive in a template)
+ */
+ constructor(parent, props) {
+ Component.current = this;
+ let constr = this.constructor;
+ const defaultProps = constr.defaultProps;
+ if (defaultProps) {
+ props = props || {};
+ this.__applyDefaultProps(props, defaultProps);
+ }
+ this.props = props;
+ if (QWeb.dev) {
+ QWeb.utils.validateProps(constr, this.props);
+ }
+ const id = nextId++;
+ let depth;
+ if (parent) {
+ this.env = parent.env;
+ const __powl__ = parent.__owl__;
+ __powl__.children[id] = this;
+ depth = __powl__.depth + 1;
+ }
+ else {
+ // we are the root component
+ this.env = this.constructor.env;
+ if (!this.env.qweb) {
+ this.env.qweb = new QWeb();
+ }
+ this.env.qweb.on("update", this, () => {
+ if (this.__owl__.isMounted) {
+ this.render(true);
+ }
+ if (this.__owl__.isDestroyed) {
+ // this is unlikely to happen, but if a root widget is destroyed,
+ // we want to remove our subscription. The usual way to do that
+ // would be to perform some check in the destroy method, but since
+ // it is very performance sensitive, and since this is a rare event,
+ // we simply do it lazily
+ this.env.qweb.off("update", this);
+ }
+ });
+ depth = 0;
+ }
+ const qweb = this.env.qweb;
+ const template = constr.template || this.__getTemplate(qweb);
+ this.__owl__ = {
+ id: id,
+ depth: depth,
+ vnode: null,
+ pvnode: null,
+ isMounted: false,
+ isDestroyed: false,
+ parent: parent || null,
+ children: {},
+ cmap: {},
+ currentFiber: null,
+ parentLastFiberId: 0,
+ boundHandlers: {},
+ mountedCB: null,
+ willUnmountCB: null,
+ willPatchCB: null,
+ patchedCB: null,
+ willStartCB: null,
+ willUpdatePropsCB: null,
+ observer: null,
+ renderFn: qweb.render.bind(qweb, template),
+ classObj: null,
+ refs: null,
+ scope: null,
+ vars: null
+ };
+ }
+ /**
+ * The `el` is the root element of the component. Note that it could be null:
+ * this is the case if the component is not mounted yet, or is destroyed.
+ */
+ get el() {
+ return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
+ }
+ /**
+ * willStart is an asynchronous hook that can be implemented to perform some
+ * action before the initial rendering of a component.
+ *
+ * It will be called exactly once before the initial rendering. It is useful
+ * in some cases, for example, to load external assets (such as a JS library)
+ * before the component is rendered.
+ *
+ * Note that a slow willStart method will slow down the rendering of the user
+ * interface. Therefore, some effort should be made to make this method as
+ * fast as possible.
+ *
+ * Note: this method should not be called manually.
+ */
+ async willStart() { }
+ /**
+ * mounted is a hook that is called each time a component is attached to the
+ * DOM. This is a good place to add some listeners, or to interact with the
+ * DOM, if the component needs to perform some measure for example.
+ *
+ * Note: this method should not be called manually.
+ *
+ * @see willUnmount
+ */
+ mounted() { }
+ /**
+ * The willUpdateProps is an asynchronous hook, called just before new props
+ * are set. This is useful if the component needs some asynchronous task
+ * performed, depending on the props (for example, assuming that the props are
+ * some record Id, fetching the record data).
+ *
+ * This hook is not called during the first render (but willStart is called
+ * and performs a similar job).
+ */
+ async willUpdateProps(nextProps) { }
+ /**
+ * The willPatch hook is called just before the DOM patching process starts.
+ * It is not called on the initial render. This is useful to get some
+ * information which are in the DOM. For example, the current position of the
+ * scrollbar
+ */
+ willPatch() { }
+ /**
+ * This hook is called whenever a component did actually update its props,
+ * state or env.
+ *
+ * This method is not called on the initial render. It is useful to interact
+ * with the DOM (for example, through an external library) whenever the
+ * component was updated.
+ *
+ * Updating the component state in this hook is possible, but not encouraged.
+ * One need to be careful, because updates here will cause rerender, which in
+ * turn will cause other calls to updated. So, we need to be particularly
+ * careful at avoiding endless cycles.
+ */
+ patched() { }
+ /**
+ * willUnmount is a hook that is called each time just before a component is
+ * unmounted from the DOM. This is a good place to remove some listeners, for
+ * example.
+ *
+ * Note: this method should not be called manually.
+ *
+ * @see mounted
+ */
+ willUnmount() { }
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+ /**
+ * Mount the component to a target element.
+ *
+ * This should only be done if the component was created manually. Components
+ * created declaratively in templates are managed by the Owl system.
+ *
+ * Note that a component can be mounted an unmounted several times
+ */
+ async mount(target) {
+ const __owl__ = this.__owl__;
+ if (__owl__.isMounted) {
+ return Promise.resolve();
+ }
+ if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
+ let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
+ message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
+ throw new Error(message);
+ }
+ const fiber = new Fiber(null, this, false, target);
+ fiber.shouldPatch = false;
+ if (!__owl__.vnode) {
+ this.__prepareAndRender(fiber, () => { });
+ }
+ else {
+ this.__render(fiber);
+ }
+ return scheduler.addFiber(fiber);
+ }
+ /**
+ * The unmount method is the opposite of the mount method. It is useful
+ * to call willUnmount calls and remove the component from the DOM.
+ */
+ unmount() {
+ if (this.__owl__.isMounted) {
+ this.__callWillUnmount();
+ this.el.remove();
+ }
+ }
+ /**
+ * The render method is the main entry point to render a component (once it
+ * is ready. This method is not initially called when the component is
+ * rendered the first time).
+ *
+ * This method will cause all its sub components to potentially rerender
+ * themselves. Note that `render` is not called if a component is updated via
+ * its props.
+ */
+ async render(force = false) {
+ const __owl__ = this.__owl__;
+ if (!__owl__.isMounted && !__owl__.currentFiber) {
+ // if we get here, this means that the component was either never mounted,
+ // or was unmounted and some state change triggered a render. Either way,
+ // we do not want to actually render anything in this case.
+ return;
+ }
+ if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
+ return scheduler.addFiber(__owl__.currentFiber.root);
+ }
+ // if we aren't mounted at this point, it implies that there is a
+ // currentFiber that is already rendered (isRendered is true), so we are
+ // about to be mounted
+ const isMounted = __owl__.isMounted;
+ const fiber = new Fiber(null, this, force, null);
+ Promise.resolve().then(() => {
+ if (__owl__.isMounted || !isMounted) {
+ // we are mounted (__owl__.isMounted), or if we are currently being
+ // mounted (!isMounted), so we call __render
+ this.__render(fiber);
+ }
+ else {
+ // we were mounted when render was called, but we aren't anymore, so we
+ // were actually about to be unmounted ; we can thus forget about this
+ // fiber
+ fiber.isCompleted = true;
+ __owl__.currentFiber = null;
+ }
+ });
+ return scheduler.addFiber(fiber);
+ }
+ /**
+ * Destroy the component. This operation is quite complex:
+ * - it recursively destroy all children
+ * - call the willUnmount hooks if necessary
+ * - remove the dom node from the dom
+ *
+ * This should only be called manually if you created the component. Most
+ * components will be automatically destroyed.
+ */
+ destroy() {
+ const __owl__ = this.__owl__;
+ if (!__owl__.isDestroyed) {
+ const el = this.el;
+ this.__destroy(__owl__.parent);
+ if (el) {
+ el.remove();
+ }
+ }
+ }
+ /**
+ * This method is called by the component system whenever its props are
+ * updated. If it returns true, then the component will be rendered.
+ * Otherwise, it will skip the rendering (also, its props will not be updated)
+ */
+ shouldUpdate(nextProps) {
+ return true;
+ }
+ /**
+ * Emit a custom event of type 'eventType' with the given 'payload' on the
+ * component's el, if it exists. However, note that the event will only bubble
+ * up to the parent DOM nodes. Thus, it must be called between mounted() and
+ * willUnmount().
+ */
+ trigger(eventType, payload) {
+ this.__trigger(this, eventType, payload);
+ }
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * Private helper to perform a full destroy, from the point of view of an Owl
+ * component. It does not remove the el (this is done only once on the top
+ * level destroyed component, for performance reasons).
+ *
+ * The job of this method is mostly to call willUnmount hooks, and to perform
+ * all necessary internal cleanup.
+ *
+ * Note that it does not call the __callWillUnmount method to avoid visiting
+ * all children many times.
+ */
+ __destroy(parent) {
+ const __owl__ = this.__owl__;
+ const isMounted = __owl__.isMounted;
+ if (isMounted) {
+ if (__owl__.willUnmountCB) {
+ __owl__.willUnmountCB();
+ }
+ this.willUnmount();
+ __owl__.isMounted = false;
+ }
+ const children = __owl__.children;
+ for (let key in children) {
+ children[key].__destroy(this);
+ }
+ if (parent) {
+ let id = __owl__.id;
+ delete parent.__owl__.children[id];
+ __owl__.parent = null;
+ }
+ __owl__.isDestroyed = true;
+ delete __owl__.vnode;
+ if (__owl__.currentFiber) {
+ __owl__.currentFiber.isCompleted = true;
+ }
+ }
+ __callMounted() {
+ const __owl__ = this.__owl__;
+ __owl__.isMounted = true;
+ __owl__.currentFiber = null;
+ this.mounted();
+ if (__owl__.mountedCB) {
+ __owl__.mountedCB();
+ }
+ }
+ __callWillUnmount() {
+ const __owl__ = this.__owl__;
+ if (__owl__.willUnmountCB) {
+ __owl__.willUnmountCB();
+ }
+ this.willUnmount();
+ __owl__.isMounted = false;
+ if (this.__owl__.currentFiber) {
+ this.__owl__.currentFiber.isCompleted = true;
+ this.__owl__.currentFiber.root.counter = 0;
+ }
+ const children = __owl__.children;
+ for (let id in children) {
+ const comp = children[id];
+ if (comp.__owl__.isMounted) {
+ comp.__callWillUnmount();
+ }
+ }
+ }
+ /**
+ * Private trigger method, allows to choose the component which triggered
+ * the event in the first place
+ */
+ __trigger(component, eventType, payload) {
+ if (this.el) {
+ const ev = new OwlEvent(component, eventType, {
+ bubbles: true,
+ cancelable: true,
+ detail: payload
+ });
+ const triggerHook = this.env[portalSymbol];
+ if (triggerHook) {
+ triggerHook(ev);
+ }
+ this.el.dispatchEvent(ev);
+ }
+ }
+ /**
+ * The __updateProps method is called by the t-component directive whenever
+ * it updates a component (so, when the parent template is rerendered).
+ */
+ async __updateProps(nextProps, parentFiber, scope, vars) {
+ this.__owl__.scope = scope;
+ this.__owl__.vars = vars;
+ const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
+ if (shouldUpdate) {
+ const __owl__ = this.__owl__;
+ const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
+ if (!parentFiber.child) {
+ parentFiber.child = fiber;
+ }
+ else {
+ parentFiber.lastChild.sibling = fiber;
+ }
+ parentFiber.lastChild = fiber;
+ const defaultProps = this.constructor.defaultProps;
+ if (defaultProps) {
+ this.__applyDefaultProps(nextProps, defaultProps);
+ }
+ if (QWeb.dev) {
+ QWeb.utils.validateProps(this.constructor, nextProps);
+ }
+ await Promise.all([
+ this.willUpdateProps(nextProps),
+ __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
+ ]);
+ if (fiber.isCompleted) {
+ return;
+ }
+ this.props = nextProps;
+ this.__render(fiber);
+ }
+ }
+ /**
+ * Main patching method. We call the virtual dom patch method here to convert
+ * a virtual dom vnode into some actual dom.
+ */
+ __patch(vnode) {
+ const __owl__ = this.__owl__;
+ const target = __owl__.vnode || document.createElement(vnode.sel);
+ __owl__.vnode = patch(target, vnode);
+ }
+ /**
+ * The __prepare method is only called by the t-component directive, when a
+ * subcomponent is created. It gets its scope and vars, if any, from the
+ * parent template.
+ */
+ __prepare(parentFiber, scope, vars, cb) {
+ this.__owl__.scope = scope;
+ this.__owl__.vars = vars;
+ const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
+ fiber.shouldPatch = false;
+ if (!parentFiber.child) {
+ parentFiber.child = fiber;
+ }
+ else {
+ parentFiber.lastChild.sibling = fiber;
+ }
+ parentFiber.lastChild = fiber;
+ this.__prepareAndRender(fiber, cb);
+ return fiber;
+ }
+ __getTemplate(qweb) {
+ let p = this.constructor;
+ if (!p.hasOwnProperty("_template")) {
+ // here, the component and none of its superclasses defines a static `template`
+ // key. So we fall back on looking for a template matching its name (or
+ // one of its subclass).
+ let template;
+ while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
+ p = p.__proto__;
+ }
+ if (p === Component) {
+ throw new Error(`Could not find template for component "${this.constructor.name}"`);
+ }
+ else {
+ p._template = template;
+ }
+ }
+ return p._template;
+ }
+ async __prepareAndRender(fiber, cb) {
+ try {
+ await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
+ }
+ catch (e) {
+ fiber.handleError(e);
+ return Promise.resolve();
+ }
+ if (this.__owl__.isDestroyed) {
+ return Promise.resolve();
+ }
+ if (!fiber.isCompleted) {
+ this.__render(fiber);
+ cb();
+ }
+ }
+ __render(fiber) {
+ const __owl__ = this.__owl__;
+ if (__owl__.observer) {
+ __owl__.observer.allowMutations = false;
+ }
+ let error;
+ try {
+ let vnode = __owl__.renderFn(this, {
+ handlers: __owl__.boundHandlers,
+ fiber: fiber
+ });
+ // we iterate over the children to detect those that no longer belong to the
+ // current rendering: those ones, if not mounted yet, can (and have to) be
+ // destroyed right now, because they are not in the DOM, and thus we won't
+ // be notified later on (when patching), that they are removed from the DOM
+ for (let childKey in __owl__.children) {
+ let child = __owl__.children[childKey];
+ if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
+ child.destroy();
+ }
+ }
+ if (!vnode) {
+ throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
+ }
+ fiber.vnode = vnode;
+ // we apply here the class information described on the component by the
+ // template (so, something like ) to the actual
+ // root vnode
+ if (__owl__.classObj) {
+ const data = vnode.data;
+ data.class = Object.assign(data.class || {}, __owl__.classObj);
+ }
+ }
+ catch (e) {
+ error = e;
+ }
+ if (__owl__.observer) {
+ __owl__.observer.allowMutations = true;
+ }
+ fiber.root.counter--;
+ fiber.isRendered = true;
+ if (error) {
+ fiber.handleError(error);
+ }
+ }
+ /**
+ * Only called by qweb t-component directive (when t-keepalive is set)
+ */
+ __remount() {
+ const __owl__ = this.__owl__;
+ if (!__owl__.isMounted) {
+ __owl__.isMounted = true;
+ this.mounted();
+ }
+ }
+ /**
+ * Apply default props (only top level).
+ *
+ * Note that this method does modify in place the props
+ */
+ __applyDefaultProps(props, defaultProps) {
+ for (let propName in defaultProps) {
+ if (props[propName] === undefined) {
+ props[propName] = defaultProps[propName];
+ }
+ }
+ }
+ }
+ Component.template = null;
+ Component._template = null;
+ Component.current = null;
+ Component.components = {};
+ Component.env = {};
+ // expose scheduler s.t. it can be mocked for testing purposes
+ Component.scheduler = scheduler;
+
+ /**
+ * The `Context` object provides a way to share data between an arbitrary number
+ * of component. Usually, data is passed from a parent to its children component,
+ * but when we have to deal with some mostly global information, this can be
+ * annoying, since each component will need to pass the information to each
+ * children, even though some or most of them will not use the information.
+ *
+ * With a `Context` object, each component can subscribe (with the `useContext`
+ * hook) to its state, and will be updated whenever the context state is updated.
+ */
+ function partitionBy(arr, fn) {
+ let lastGroup = false;
+ let lastValue;
+ return arr.reduce((acc, cur) => {
+ let curVal = fn(cur);
+ if (lastGroup) {
+ if (curVal === lastValue) {
+ lastGroup.push(cur);
+ }
+ else {
+ lastGroup = false;
+ }
+ }
+ if (!lastGroup) {
+ lastGroup = [cur];
+ acc.push(lastGroup);
+ }
+ lastValue = curVal;
+ return acc;
+ }, []);
+ }
+ class Context extends EventBus {
+ constructor(state = {}) {
+ super();
+ this.rev = 1;
+ // mapping from component id to last observed context id
+ this.mapping = {};
+ this.observer = new Observer();
+ this.observer.notifyCB = () => {
+ // notify components in the next microtask tick to ensure that subscribers
+ // are notified only once for all changes that occur in the same micro tick
+ let rev = this.rev;
+ return Promise.resolve().then(() => {
+ if (rev === this.rev) {
+ this.__notifyComponents();
+ }
+ });
+ };
+ this.state = this.observer.observe(state);
+ this.subscriptions.update = [];
+ }
+ /**
+ * Instead of using trigger to emit an update event, we actually implement
+ * our own function to do that. The reason is that we need to be smarter than
+ * a simple trigger function: we need to wait for parent components to be
+ * done before doing children components. More precisely, if an update
+ * as an effect of destroying a children, we do not want to call any code
+ * from the child, and certainly not render it.
+ *
+ * This method implements a simple grouping algorithm by depth. If we have
+ * connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
+ * them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
+ * be updated sequentially, but each components in a given group will be done in
+ * parallel.
+ *
+ * This is a very simple algorithm, but it avoids checking if a given
+ * component is a child of another.
+ */
+ async __notifyComponents() {
+ const rev = ++this.rev;
+ const subscriptions = this.subscriptions.update;
+ const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
+ for (let group of groups) {
+ const proms = group.map(sub => sub.callback.call(sub.owner, rev));
+ // at this point, each component in the current group has registered a
+ // top level fiber in the scheduler. It could happen that rendering these
+ // components is done (if they have no children). This is why we manually
+ // flush the scheduler. This will force the scheduler to check
+ // immediately if they are done, which will cause their rendering
+ // promise to resolve earlier, which means that there is a chance of
+ // processing the next group in the same frame.
+ scheduler.flush();
+ await Promise.all(proms);
+ }
+ }
+ }
+ /**
+ * The`useContext` hook is the normal way for a component to register themselve
+ * to context state changes. The `useContext` method returns the context state
+ */
+ function useContext(ctx) {
+ const component = Component.current;
+ return useContextWithCB(ctx, component, component.render.bind(component));
+ }
+ function useContextWithCB(ctx, component, method) {
+ const __owl__ = component.__owl__;
+ const id = __owl__.id;
+ const mapping = ctx.mapping;
+ if (id in mapping) {
+ return ctx.state;
+ }
+ if (!__owl__.observer) {
+ __owl__.observer = new Observer();
+ __owl__.observer.notifyCB = component.render.bind(component);
+ }
+ const currentCB = __owl__.observer.notifyCB;
+ __owl__.observer.notifyCB = function () {
+ if (ctx.rev > mapping[id]) {
+ // in this case, the context has been updated since we were rendering
+ // last, and we do not need to render here with the observer. A
+ // rendering is coming anyway, with the correct props.
+ return;
+ }
+ currentCB();
+ };
+ mapping[id] = 0;
+ const renderFn = __owl__.renderFn;
+ __owl__.renderFn = function (comp, params) {
+ mapping[id] = ctx.rev;
+ return renderFn(comp, params);
+ };
+ ctx.on("update", component, async (contextRev) => {
+ if (mapping[id] < contextRev) {
+ mapping[id] = contextRev;
+ await method();
+ }
+ });
+ const __destroy = component.__destroy;
+ component.__destroy = parent => {
+ ctx.off("update", component);
+ delete mapping[id];
+ __destroy.call(component, parent);
+ };
+ return ctx.state;
+ }
+
+ /**
+ * Owl Hook System
+ *
+ * This file introduces the concept of hooks, similar to React or Vue hooks.
+ * We have currently an implementation of:
+ * - useState (reactive state)
+ * - onMounted
+ * - onWillUnmount
+ * - useRef
+ */
+ // -----------------------------------------------------------------------------
+ // useState
+ // -----------------------------------------------------------------------------
+ /**
+ * This is the main way a component can be made reactive. The useState hook
+ * will return an observed object (or array). Changes to that value will then
+ * trigger a rerendering of the current component.
+ */
+ function useState(state) {
+ const component = Component.current;
+ const __owl__ = component.__owl__;
+ if (!__owl__.observer) {
+ __owl__.observer = new Observer();
+ __owl__.observer.notifyCB = component.render.bind(component);
+ }
+ return __owl__.observer.observe(state);
+ }
+ // -----------------------------------------------------------------------------
+ // Life cycle hooks
+ // -----------------------------------------------------------------------------
+ function makeLifecycleHook(method, reverse = false) {
+ if (reverse) {
+ return function (cb) {
+ const component = Component.current;
+ if (component.__owl__[method]) {
+ const current = component.__owl__[method];
+ component.__owl__[method] = function () {
+ current.call(component);
+ cb.call(component);
+ };
+ }
+ else {
+ component.__owl__[method] = cb;
+ }
+ };
+ }
+ else {
+ return function (cb) {
+ const component = Component.current;
+ if (component.__owl__[method]) {
+ const current = component.__owl__[method];
+ component.__owl__[method] = function () {
+ cb.call(component);
+ current.call(component);
+ };
+ }
+ else {
+ component.__owl__[method] = cb;
+ }
+ };
+ }
+ }
+ function makeAsyncHook(method) {
+ return function (cb) {
+ const component = Component.current;
+ if (component.__owl__[method]) {
+ const current = component.__owl__[method];
+ component.__owl__[method] = function (...args) {
+ return Promise.all[(current.call(component, ...args), cb.call(component, ...args))];
};
}
+ else {
+ component.__owl__[method] = cb;
+ }
+ };
+ }
+ const onMounted = makeLifecycleHook("mountedCB", true);
+ const onWillUnmount = makeLifecycleHook("willUnmountCB");
+ const onWillPatch = makeLifecycleHook("willPatchCB");
+ const onPatched = makeLifecycleHook("patchedCB", true);
+ const onWillStart = makeAsyncHook("willStartCB");
+ const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
+ function useRef(name) {
+ const __owl__ = Component.current.__owl__;
+ return {
+ get el() {
+ const val = __owl__.refs && __owl__.refs[name];
+ if (val instanceof HTMLElement) {
+ return val;
+ }
+ else if (val instanceof Component) {
+ return val.el;
+ }
+ return null;
+ },
+ get comp() {
+ const val = __owl__.refs && __owl__.refs[name];
+ return val instanceof Component ? val : null;
+ }
+ };
+ }
+ // -----------------------------------------------------------------------------
+ // useSubEnv
+ // -----------------------------------------------------------------------------
+ /**
+ * This hook is a simple way to let components use a sub environment. Note that
+ * like for all hooks, it is important that this is only called in the
+ * constructor method.
+ */
+ function useSubEnv(nextEnv) {
+ const component = Component.current;
+ component.env = Object.assign(Object.create(component.env), nextEnv);
+ }
+
+ var _hooks = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ useState: useState,
+ onMounted: onMounted,
+ onWillUnmount: onWillUnmount,
+ onWillPatch: onWillPatch,
+ onPatched: onPatched,
+ onWillStart: onWillStart,
+ onWillUpdateProps: onWillUpdateProps,
+ useRef: useRef,
+ useSubEnv: useSubEnv
+ });
+
+ class Store extends Context {
+ constructor(config) {
+ super(config.state);
+ this.actions = config.actions;
+ this.env = config.env;
+ this.getters = {};
+ this.updateFunctions = [];
+ if (config.getters) {
+ const firstArg = {
+ state: this.state,
+ getters: this.getters
+ };
+ for (let g in config.getters) {
+ this.getters[g] = config.getters[g].bind(this, firstArg);
+ }
+ }
}
dispatch(action, ...payload) {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
const result = this.actions[action]({
- commit: this.commit.bind(this),
dispatch: this.dispatch.bind(this),
env: this.env,
state: this.state,
getters: this.getters
}, ...payload);
- if (result instanceof Promise) {
- return new Promise((resolve, reject) => {
- result.then(() => resolve());
- result.catch(reject);
- });
- }
+ return result;
}
- commit(type, ...payload) {
- if (!this.mutations[type]) {
- throw new Error(`[Error] mutation ${type} is undefined`);
- }
- this._commitLevel++;
- this.observer.allowMutations = true;
- const res = this.mutations[type].call(null, {
- commit: this.commit.bind(this),
- state: this.state,
- getters: this.getters
- }, ...payload);
- if (this._commitLevel === 1) {
- this.observer.allowMutations = false;
- if (this.debug) {
- this.history.push({
- state: this.state,
- mutation: type,
- payload: [...payload]
- });
+ }
+ const isStrictEqual = (a, b) => a === b;
+ function useStore(selector, options = {}) {
+ const component = Component.current;
+ const componentId = component.__owl__.id;
+ const store = options.store || component.env.store;
+ if (!(store instanceof Store)) {
+ throw new Error(`No store found when connecting '${component.constructor.name}'`);
+ }
+ let result = selector(store.state, component.props);
+ const hashFn = store.observer.revNumber.bind(store.observer);
+ let revNumber = hashFn(result);
+ const isEqual = options.isEqual || isStrictEqual;
+ if (!store.updateFunctions[componentId]) {
+ store.updateFunctions[componentId] = [];
+ }
+ function selectCompareUpdate(state, props) {
+ const oldResult = result;
+ result = selector(state, props);
+ const newRevNumber = hashFn(result);
+ if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
+ revNumber = newRevNumber;
+ if (options.onUpdate) {
+ options.onUpdate(result);
}
+ return true;
}
- this._commitLevel--;
- return res;
+ return false;
+ }
+ store.updateFunctions[componentId].push(function () {
+ return selectCompareUpdate(store.state, component.props);
+ });
+ useContextWithCB(store, component, function () {
+ let shouldRender = false;
+ for (let fn of store.updateFunctions[componentId]) {
+ shouldRender = fn() || shouldRender;
+ }
+ if (shouldRender) {
+ return component.render();
+ }
+ });
+ onWillUpdateProps(props => {
+ selectCompareUpdate(store.state, props);
+ });
+ const __destroy = component.__destroy;
+ component.__destroy = parent => {
+ delete store.updateFunctions[componentId];
+ __destroy.call(component, parent);
+ };
+ if (typeof result !== "object") {
+ return result;
+ }
+ return new Proxy(result, {
+ get(target, k) {
+ return result[k];
+ },
+ set(target, k, v) {
+ throw new Error("Store state should only be modified through actions");
+ }
+ });
+ }
+ function useDispatch(store) {
+ store = store || Component.current.env.store;
+ return store.dispatch.bind(store);
+ }
+ function useGetters(store) {
+ store = store || Component.current.env.store;
+ return store.getters;
+ }
+
+ /**
+ * Owl Tags
+ *
+ * We have here a (very) small collection of tag functions:
+ *
+ * - xml
+ *
+ * The plan is to add a few other tags such as css, globalcss.
+ */
+ /**
+ * XML tag helper for defining templates. With this, one can simply define
+ * an inline template with just the template xml:
+ * ```js
+ * class A extends Component {
+ * static template = xml`
some template
`;
+ * }
+ * ```
+ */
+ function xml(strings, ...args) {
+ const name = `__template__${QWeb.nextId++}`;
+ const value = String.raw(strings, ...args);
+ QWeb.registerTemplate(name, value);
+ return name;
+ }
+
+ var _tags = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ xml: xml
+ });
+
+ /**
+ * AsyncRoot
+ *
+ * Owl is by default asynchronous, and the user interface will wait for all its
+ * subcomponents to be rendered before updating the DOM. This is most of the
+ * time what we want, but in some cases, it makes sense to "detach" a component
+ * from this coordination. This is the goal of the AsyncRoot component.
+ */
+ class AsyncRoot extends Component {
+ async __updateProps(nextProps, parentFiber) {
+ this.render(parentFiber.force);
+ }
+ }
+ AsyncRoot.template = xml ``;
+
+ /**
+ * Portal
+ *
+ * The Portal component allows to render a part of a component outside it's DOM.
+ * It is for example useful for dialogs: for css reasons, dialogs are in general
+ * placed in a specific spot of the DOM (e.g. directly in the body). With the
+ * Portal, a component can conditionally specify in its tempate that it contains
+ * a dialog, and where this dialog should be inserted in the DOM.
+ *
+ * The Portal component ensures that the communication between the content of
+ * the Portal and its parent properly works: business events reaching the Portal
+ * are re-triggered on an empty node located in the parent's DOM.
+ */
+ class Portal extends Component {
+ constructor(parent, props) {
+ super(parent, props);
+ // boolean to indicate whether or not we must listen to 'dom-appended' event
+ // to hook on the moment when the target is inserted into the DOM (because it
+ // is not when the portal is rendered)
+ this.doTargetLookUp = true;
+ // set of encountered events that need to be redirected
+ this._handledEvents = new Set();
+ // function that will be the event's tunnel (needs to be an arrow function to
+ // avoid having to rebind `this`)
+ this._handlerTunnel = (ev) => {
+ ev.stopPropagation();
+ this.__trigger(ev.originalComponent, ev.type, ev.detail);
+ };
+ // Storing the parent's env
+ this.parentEnv = null;
+ // represents the element that is moved somewhere else
+ this.portal = null;
+ // the target where we will move `portal`
+ this.target = null;
+ this.parentEnv = parent ? parent.env : {};
+ // put a callback in the env that is propagated to children s.t. portal can
+ // register an handler to those events just before children will trigger them
+ useSubEnv({
+ [portalSymbol]: ev => {
+ if (!this._handledEvents.has(ev.type)) {
+ this.portal.elm.addEventListener(ev.type, this._handlerTunnel);
+ this._handledEvents.add(ev.type);
+ }
+ }
+ });
}
/**
- * Instead of using trigger to emit an update event, we actually implement
- * our own function to do that. The reason is that we need to be smarter than
- * a simple trigger function: we need to wait for parent components to be
- * done before doing children components. The reason is that if an update
- * as an effect of destroying a children, we do not want to call the
- * mapStoreToProps function of the child, nor rendering it.
- *
- * This method is not optimal if we have a bunch of asynchronous components:
- * we wait sequentially for each component to be completed before updating the
- * next. However, the only things that matters is that children are updated
- * after their parents. So, this could be optimized by being smarter, and
- * updating all widgets concurrently, except for parents/children.
+ * At each DOM change, we must ensure that the portal contains exactly one
+ * child
*/
- async __notifyComponents() {
- this._updateId++;
- const current = this._updateId;
- this._gettersCache = {};
- const subs = this.subscriptions.update || [];
- for (let i = 0, iLen = subs.length; i < iLen; i++) {
- const sub = subs[i];
- const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
- if (shouldCallback) {
- await sub.callback.call(sub.owner, current);
+ __checkVNodeStructure(vnode) {
+ const children = vnode.children;
+ let countRealNodes = 0;
+ for (let child of children) {
+ if (child.sel) {
+ countRealNodes++;
}
}
+ if (countRealNodes !== 1) {
+ throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
+ }
}
- }
- //------------------------------------------------------------------------------
- // Connect function
- //------------------------------------------------------------------------------
- function revNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.rev;
+ /**
+ * Ensure the target is still there at whichever time we render
+ */
+ __checkTargetPresence() {
+ if (!this.target || !document.contains(this.target)) {
+ throw new Error(`Could not find any match for "${this.props.target}"`);
+ }
}
- return 0;
- }
- function deepRevNumber(o) {
- if (o !== null && typeof o === "object" && o.__owl__) {
- return o.__owl__.deepRev;
+ /**
+ * Move the portal's element to the target
+ */
+ __deployPortal() {
+ this.__checkTargetPresence();
+ this.target.appendChild(this.portal.elm);
}
- return 0;
- }
- function connect(Comp, mapStoreToProps, options = {}) {
- let hashFunction = options.hashFunction || null;
- const getStore = options.getStore || (env => env.store);
- if (!hashFunction) {
- let deep = "deep" in options ? options.deep : true;
- let defaultRevFunction = deep ? deepRevNumber : revNumber;
- hashFunction = function ({ storeProps }, options) {
- const { currentStoreProps } = options;
- if ("__owl__" in storeProps) {
- return defaultRevFunction(storeProps);
+ /**
+ * Override to remove from the DOM the element we have teleported
+ *
+ * @override
+ */
+ __destroy(parent) {
+ if (this.portal && this.portal.elm) {
+ const displacedElm = this.portal.elm;
+ const parent = displacedElm.parentNode;
+ if (parent) {
+ parent.removeChild(displacedElm);
}
- let hash = 0;
- for (let key in storeProps) {
- const val = storeProps[key];
- const hashVal = defaultRevFunction(val);
- if (hashVal === 0) {
- if (val !== currentStoreProps[key]) {
- options.didChange = true;
- }
- }
- else {
- hash += hashVal;
- }
- }
- return hash;
- };
+ }
+ super.__destroy(parent);
}
- const Result = class extends Comp {
- constructor(parent, props) {
- const env = parent instanceof Component ? parent.env : parent;
- const store = getStore(env);
- const ownProps = Object.assign({}, props || {});
- const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
- const mergedProps = Object.assign({}, props || {}, storeProps);
- super(parent, mergedProps);
- this.__owl__.ownProps = ownProps;
- this.__owl__.currentStoreProps = storeProps;
- this.__owl__.store = store;
- this.__owl__.storeHash = hashFunction({
- state: store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, {
- currentStoreProps: storeProps
- });
+ /**
+ * Override to patch the element that has been teleported
+ *
+ * @override
+ */
+ __patch(vnode) {
+ if (this.doTargetLookUp) {
+ const target = document.querySelector(this.props.target);
+ if (!target) {
+ this.env.qweb.on("dom-appended", this, () => {
+ this.doTargetLookUp = false;
+ this.env.qweb.off("dom-appended", this);
+ this.target = document.querySelector(this.props.target);
+ this.__deployPortal();
+ });
+ }
+ else {
+ this.doTargetLookUp = false;
+ this.target = target;
+ }
}
- /**
- * We do not use the mounted hook here for a subtle reason: we want the
- * updates to be called for the parents before the children. However,
- * if we use the mounted hook, this will be done in the reverse order.
- */
- __callMounted() {
- this.__owl__.store.on("update", this, this.__checkUpdate);
- super.__callMounted();
+ this.__checkVNodeStructure(vnode);
+ const shouldDeploy = !this.portal && !this.doTargetLookUp;
+ if (!this.doTargetLookUp && !shouldDeploy) {
+ // Only on pure patching, provided the
+ // this.target's parent has not been unmounted
+ this.__checkTargetPresence();
}
- willUnmount() {
- this.__owl__.store.off("update", this);
- super.willUnmount();
+ const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
+ this.portal = patch(portalPatch, vnode.children[0]);
+ vnode.children = [];
+ super.__patch(vnode);
+ if (shouldDeploy) {
+ this.__deployPortal();
}
- async __checkUpdate(updateId) {
- if (updateId === this.__owl__.currentUpdateId) {
+ }
+ /**
+ * Override to set the env
+ */
+ __trigger(component, eventType, payload) {
+ const env = this.env;
+ this.env = this.parentEnv;
+ super.__trigger(component, eventType, payload);
+ this.env = env;
+ }
+ }
+ Portal.template = xml ``;
+ Portal.props = {
+ target: {
+ type: String
+ }
+ };
+
+ class Link extends Component {
+ constructor() {
+ super(...arguments);
+ this.href = this.env.router.destToPath(this.props);
+ }
+ async willUpdateProps(nextProps) {
+ this.href = this.env.router.destToPath(nextProps);
+ }
+ get isActive() {
+ if (this.env.router.mode === "hash") {
+ return document.location.hash === this.href;
+ }
+ return document.location.pathname === this.href;
+ }
+ navigate(ev) {
+ // don't redirect with control keys
+ if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
+ return;
+ }
+ // don't redirect on right click
+ if (ev.button !== undefined && ev.button !== 0) {
+ return;
+ }
+ // don't redirect if `target="_blank"`
+ if (ev.currentTarget && ev.currentTarget.getAttribute) {
+ const target = ev.currentTarget.getAttribute("target");
+ if (/\b_blank\b/i.test(target)) {
return;
}
- const ownProps = this.__owl__.ownProps;
- const storeProps = mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters);
- const options = {
- currentStoreProps: this.__owl__.currentStoreProps
- };
- const storeHash = hashFunction({
- state: this.__owl__.store.state,
- storeProps: storeProps,
- revNumber,
- deepRevNumber
- }, options);
- let didChange = options.didChange;
- if (storeHash !== this.__owl__.storeHash) {
- didChange = true;
- this.__owl__.storeHash = storeHash;
- }
- if (didChange) {
- this.__owl__.currentStoreProps = storeProps;
- await this.__updateProps(ownProps, false);
- }
}
- __updateProps(nextProps, forceUpdate, patchQueue) {
- const __owl__ = this.__owl__;
- __owl__.currentUpdateId = __owl__.store._updateId;
- if (__owl__.ownProps !== nextProps) {
- __owl__.currentStoreProps = mapStoreToProps(__owl__.store.state, nextProps, __owl__.store.getters);
- }
- __owl__.ownProps = nextProps;
- const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
- return super.__updateProps(mergedProps, forceUpdate, patchQueue);
- }
- };
- // we assign here a unique name to the resulting anonymous class.
- // this is necessary for Owl to be able to properly deduce templates.
- // Otherwise, all connected components would have the same name, and then
- // each component after the first will necessarily have the same template.
- let name = `Connected${Comp.name}`;
- Object.defineProperty(Result, "name", { value: name });
- return Result;
+ ev.preventDefault();
+ this.env.router.navigate(this.props);
+ }
}
+ Link.template = xml `
+
+
+
+ `;
- /**
- * Owl Utils
- *
- * We have here a small collection of utility functions:
- *
- * - whenReady
- * - loadJS
- * - loadTemplates
- * - escape
- * - debounce
- */
- function whenReady(fn) {
- return new Promise(function (resolve) {
- if (document.readyState !== "loading") {
- resolve();
+ class RouteComponent extends Component {
+ get routeComponent() {
+ return this.env.router.currentRoute && this.env.router.currentRoute.component;
+ }
+ }
+ RouteComponent.template = xml `
+
+
+
+ `;
+
+ const paramRegexp = /\{\{(.*?)\}\}/;
+ class Router {
+ constructor(env, routes, options = { mode: "history" }) {
+ this.currentRoute = null;
+ this.currentParams = null;
+ env.router = this;
+ this.mode = options.mode;
+ this.env = env;
+ this.routes = {};
+ this.routeIds = [];
+ let nextId = 1;
+ for (let partialRoute of routes) {
+ if (!partialRoute.name) {
+ partialRoute.name = "__route__" + nextId++;
+ }
+ if (partialRoute.component) {
+ QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
+ }
+ if (partialRoute.redirect) {
+ this.validateDestination(partialRoute.redirect);
+ }
+ partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
+ this.routes[partialRoute.name] = partialRoute;
+ this.routeIds.push(partialRoute.name);
}
- else {
- document.addEventListener("DOMContentLoaded", resolve, false);
+ }
+ //--------------------------------------------------------------------------
+ // Public API
+ //--------------------------------------------------------------------------
+ async start() {
+ this._listener = ev => this._navigate(this.currentPath(), ev);
+ window.addEventListener("popstate", this._listener);
+ if (this.mode === "hash") {
+ window.addEventListener("hashchange", this._listener);
}
- }).then(fn || function () { });
- }
- const loadedScripts = {};
- function loadJS(url) {
- if (url in loadedScripts) {
- return loadedScripts[url];
- }
- const promise = new Promise(function (resolve, reject) {
- const script = document.createElement("script");
- script.type = "text/javascript";
- script.src = url;
- script.onload = function () {
- resolve();
- };
- script.onerror = function () {
- reject(`Error loading file '${url}'`);
- };
- const head = document.head || document.getElementsByTagName("head")[0];
- head.appendChild(script);
- });
- loadedScripts[url] = promise;
- return promise;
- }
- async function loadTemplates(url) {
- const result = await fetch(url);
- if (!result.ok) {
- throw new Error("Error while fetching xml templates");
- }
- let templates = await result.text();
- templates = templates.replace(//g, "");
- return templates;
- }
- function escape(str) {
- if (str === undefined) {
- return "";
- }
- if (typeof str === "number") {
- return String(str);
- }
- return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, "'")
- .replace(/`/g, "`");
- }
- /**
- * Returns a function, that, as long as it continues to be invoked, will not
- * be triggered. The function will be called after it stops being called for
- * N milliseconds. If `immediate` is passed, trigger the function on the
- * leading edge, instead of the trailing.
- *
- * Inspired by https://davidwalsh.name/javascript-debounce-function
- */
- function debounce(func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- function later() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
+ const result = await this.matchAndApplyRules(this.currentPath());
+ if (result.type === "match") {
+ this.currentRoute = result.route;
+ this.currentParams = result.params;
+ const currentPath = this.routeToPath(result.route, result.params);
+ if (currentPath !== this.currentPath()) {
+ this.setUrlFromPath(currentPath);
}
}
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
+ }
+ async navigate(to) {
+ const path = this.destToPath(to);
+ return this._navigate(path);
+ }
+ async _navigate(path, ev) {
+ const initialName = this.currentRouteName;
+ const initialParams = this.currentParams;
+ const result = await this.matchAndApplyRules(path);
+ if (result.type === "match") {
+ const finalPath = this.routeToPath(result.route, result.params);
+ const isPopStateEvent = ev && ev instanceof PopStateEvent;
+ if (!isPopStateEvent) {
+ this.setUrlFromPath(finalPath);
+ }
+ this.currentRoute = result.route;
+ this.currentParams = result.params;
}
- };
+ else if (result.type === "nomatch") {
+ this.currentRoute = null;
+ this.currentParams = null;
+ }
+ const didChange = this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
+ if (didChange) {
+ this.env.qweb.forceUpdate();
+ return true;
+ }
+ return false;
+ }
+ destToPath(dest) {
+ this.validateDestination(dest);
+ return dest.path || this.routeToPath(this.routes[dest.to], dest.params);
+ }
+ get currentRouteName() {
+ return this.currentRoute && this.currentRoute.name;
+ }
+ //--------------------------------------------------------------------------
+ // Private helpers
+ //--------------------------------------------------------------------------
+ setUrlFromPath(path) {
+ const separator = this.mode === "hash" ? location.pathname : "";
+ const url = location.origin + separator + path;
+ if (url !== window.location.href) {
+ window.history.pushState({}, path, url);
+ }
+ }
+ validateDestination(dest) {
+ if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
+ throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
+ }
+ }
+ routeToPath(route, params) {
+ const path = route.path;
+ const parts = path.split("/");
+ const l = parts.length;
+ for (let i = 0; i < l; i++) {
+ const part = parts[i];
+ const match = part.match(paramRegexp);
+ if (match) {
+ const key = match[1].split(".")[0];
+ parts[i] = params[key];
+ }
+ }
+ const prefix = this.mode === "hash" ? "#" : "";
+ return prefix + parts.join("/");
+ }
+ currentPath() {
+ let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
+ return result || "/";
+ }
+ match(path) {
+ for (let routeId of this.routeIds) {
+ let route = this.routes[routeId];
+ let params = this.getRouteParams(route, path);
+ if (params) {
+ return {
+ type: "match",
+ route: route,
+ params: params
+ };
+ }
+ }
+ return { type: "nomatch" };
+ }
+ async matchAndApplyRules(path) {
+ const result = this.match(path);
+ if (result.type === "match") {
+ return this.applyRules(result);
+ }
+ return result;
+ }
+ async applyRules(matchResult) {
+ const route = matchResult.route;
+ if (route.redirect) {
+ const path = this.destToPath(route.redirect);
+ return this.matchAndApplyRules(path);
+ }
+ if (route.beforeRouteEnter) {
+ const result = await route.beforeRouteEnter({
+ env: this.env,
+ from: this.currentRoute,
+ to: route
+ });
+ if (result === false) {
+ return { type: "cancelled" };
+ }
+ else if (result !== true) {
+ // we want to navigate to another destination
+ const path = this.destToPath(result);
+ return this.matchAndApplyRules(path);
+ }
+ }
+ return matchResult;
+ }
+ getRouteParams(route, path) {
+ if (route.path === "*") {
+ return {};
+ }
+ if (path.startsWith("#")) {
+ path = path.slice(1);
+ }
+ const descrParts = route.path.split("/");
+ const targetParts = path.split("/");
+ const l = descrParts.length;
+ if (l !== targetParts.length) {
+ return false;
+ }
+ const result = {};
+ for (let i = 0; i < l; i++) {
+ const descr = descrParts[i];
+ let target = targetParts[i];
+ const match = descr.match(paramRegexp);
+ if (match) {
+ const [key, suffix] = match[1].split(".");
+ if (suffix === "number") {
+ target = parseInt(target, 10);
+ }
+ result[key] = target;
+ }
+ else if (descr !== target) {
+ return false;
+ }
+ }
+ return result;
+ }
+ }
+ function findParams(str) {
+ const globalParamRegexp = /\{\{(.*?)\}\}/g;
+ const result = [];
+ let m;
+ do {
+ m = globalParamRegexp.exec(str);
+ if (m) {
+ result.push(m[1].split(".")[0]);
+ }
+ } while (m);
+ return result;
}
-
- var _utils = /*#__PURE__*/Object.freeze({
- whenReady: whenReady,
- loadJS: loadJS,
- loadTemplates: loadTemplates,
- escape: escape,
- debounce: debounce
- });
/**
* This file is the main file packaged by rollup (see rollup.config.js). From
@@ -3583,36 +4828,39 @@
*
* Note that dynamic values, such as a date or a commit hash are added by rollup
*/
- const __info__ = {};
- Object.defineProperty(__info__, "mode", {
- get() {
- return QWeb.dev ? "dev" : "prod";
- },
- set(mode) {
- QWeb.dev = mode === "dev";
- if (QWeb.dev) {
- const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
- console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
- }
- else {
- console.log(`Owl is now running in 'prod' mode.`);
- }
- }
- });
+ const Context$1 = Context;
+ const useState$1 = useState;
+ const core = { EventBus, Observer };
+ const router = { Router, RouteComponent, Link };
+ const Store$1 = Store;
const utils = _utils;
+ const tags = _tags;
+ const misc = { AsyncRoot, Portal };
+ const hooks$1 = Object.assign({}, _hooks, {
+ useContext: useContext,
+ useDispatch: useDispatch,
+ useGetters: useGetters,
+ useStore: useStore
+ });
+ const __info__ = {};
exports.Component = Component;
- exports.EventBus = EventBus;
- exports.Observer = Observer;
+ exports.Context = Context$1;
exports.QWeb = QWeb;
- exports.Store = Store;
+ exports.Store = Store$1;
exports.__info__ = __info__;
- exports.connect = connect;
+ exports.config = config;
+ exports.core = core;
+ exports.hooks = hooks$1;
+ exports.misc = misc;
+ exports.router = router;
+ exports.tags = tags;
+ exports.useState = useState$1;
exports.utils = utils;
- exports.__info__.version = '0.16.0';
- exports.__info__.date = '2019-06-28T09:20:47.478Z';
- exports.__info__.hash = 'd381e85';
+ exports.__info__.version = '1.0.0-beta1';
+ exports.__info__.date = '2019-12-03T07:54:22.133Z';
+ exports.__info__.hash = '42aa9d3';
exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {}));
diff --git a/tools/benchmarks/owl-0.16.0/templates.xml b/tools/benchmarks/owl-1.0.0-beta1/templates.xml
similarity index 88%
rename from tools/benchmarks/owl-0.16.0/templates.xml
rename to tools/benchmarks/owl-1.0.0-beta1/templates.xml
index 0a680ea0..c8724c0a 100644
--- a/tools/benchmarks/owl-0.16.0/templates.xml
+++ b/tools/benchmarks/owl-1.0.0-beta1/templates.xml
@@ -12,11 +12,11 @@