6224 lines
249 KiB
JavaScript
6224 lines
249 KiB
JavaScript
|
(function (exports) {
|
||
|
'use strict';
|
||
|
|
||
|
function filterOutModifiersFromData(dataList) {
|
||
|
dataList = dataList.slice();
|
||
|
const modifiers = [];
|
||
|
let elm;
|
||
|
while ((elm = dataList[0]) && typeof elm === "string") {
|
||
|
modifiers.push(dataList.shift());
|
||
|
}
|
||
|
return { modifiers, data: dataList };
|
||
|
}
|
||
|
const config = {
|
||
|
// whether or not blockdom should normalize DOM whenever a block is created.
|
||
|
// Normalizing dom mean removing empty text nodes (or containing only spaces)
|
||
|
shouldNormalizeDom: true,
|
||
|
// this is the main event handler. Every event handler registered with blockdom
|
||
|
// will go through this function, giving it the data registered in the block
|
||
|
// and the event
|
||
|
mainEventHandler: (data, ev, currentTarget) => {
|
||
|
if (typeof data === "function") {
|
||
|
data(ev);
|
||
|
}
|
||
|
else if (Array.isArray(data)) {
|
||
|
data = filterOutModifiersFromData(data).data;
|
||
|
data[0](data[1], ev);
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Toggler node
|
||
|
// -----------------------------------------------------------------------------
|
||
|
class VToggler {
|
||
|
constructor(key, child) {
|
||
|
this.key = key;
|
||
|
this.child = child;
|
||
|
}
|
||
|
mount(parent, afterNode) {
|
||
|
this.parentEl = parent;
|
||
|
this.child.mount(parent, afterNode);
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent) {
|
||
|
this.child.moveBeforeDOMNode(node, parent);
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
this.moveBeforeDOMNode((other && other.firstNode()) || afterNode);
|
||
|
}
|
||
|
patch(other, withBeforeRemove) {
|
||
|
if (this === other) {
|
||
|
return;
|
||
|
}
|
||
|
let child1 = this.child;
|
||
|
let child2 = other.child;
|
||
|
if (this.key === other.key) {
|
||
|
child1.patch(child2, withBeforeRemove);
|
||
|
}
|
||
|
else {
|
||
|
child2.mount(this.parentEl, child1.firstNode());
|
||
|
if (withBeforeRemove) {
|
||
|
child1.beforeRemove();
|
||
|
}
|
||
|
child1.remove();
|
||
|
this.child = child2;
|
||
|
this.key = other.key;
|
||
|
}
|
||
|
}
|
||
|
beforeRemove() {
|
||
|
this.child.beforeRemove();
|
||
|
}
|
||
|
remove() {
|
||
|
this.child.remove();
|
||
|
}
|
||
|
firstNode() {
|
||
|
return this.child.firstNode();
|
||
|
}
|
||
|
toString() {
|
||
|
return this.child.toString();
|
||
|
}
|
||
|
}
|
||
|
function toggler(key, child) {
|
||
|
return new VToggler(key, child);
|
||
|
}
|
||
|
|
||
|
// Custom error class that wraps error that happen in the owl lifecycle
|
||
|
class OwlError extends Error {
|
||
|
}
|
||
|
|
||
|
const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;
|
||
|
const tokenList = DOMTokenList.prototype;
|
||
|
const tokenListAdd = tokenList.add;
|
||
|
const tokenListRemove = tokenList.remove;
|
||
|
const isArray = Array.isArray;
|
||
|
const { split, trim } = String.prototype;
|
||
|
const wordRegexp = /\s+/;
|
||
|
/**
|
||
|
* We regroup here all code related to updating attributes in a very loose sense:
|
||
|
* attributes, properties and classs are all managed by the functions in this
|
||
|
* file.
|
||
|
*/
|
||
|
function setAttribute(key, value) {
|
||
|
switch (value) {
|
||
|
case false:
|
||
|
case undefined:
|
||
|
removeAttribute.call(this, key);
|
||
|
break;
|
||
|
case true:
|
||
|
elemSetAttribute.call(this, key, "");
|
||
|
break;
|
||
|
default:
|
||
|
elemSetAttribute.call(this, key, value);
|
||
|
}
|
||
|
}
|
||
|
function createAttrUpdater(attr) {
|
||
|
return function (value) {
|
||
|
setAttribute.call(this, attr, value);
|
||
|
};
|
||
|
}
|
||
|
function attrsSetter(attrs) {
|
||
|
if (isArray(attrs)) {
|
||
|
if (attrs[0] === "class") {
|
||
|
setClass.call(this, attrs[1]);
|
||
|
}
|
||
|
else {
|
||
|
setAttribute.call(this, attrs[0], attrs[1]);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
for (let k in attrs) {
|
||
|
if (k === "class") {
|
||
|
setClass.call(this, attrs[k]);
|
||
|
}
|
||
|
else {
|
||
|
setAttribute.call(this, k, attrs[k]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function attrsUpdater(attrs, oldAttrs) {
|
||
|
if (isArray(attrs)) {
|
||
|
const name = attrs[0];
|
||
|
const val = attrs[1];
|
||
|
if (name === oldAttrs[0]) {
|
||
|
if (val === oldAttrs[1]) {
|
||
|
return;
|
||
|
}
|
||
|
if (name === "class") {
|
||
|
updateClass.call(this, val, oldAttrs[1]);
|
||
|
}
|
||
|
else {
|
||
|
setAttribute.call(this, name, val);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
removeAttribute.call(this, oldAttrs[0]);
|
||
|
setAttribute.call(this, name, val);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
for (let k in oldAttrs) {
|
||
|
if (!(k in attrs)) {
|
||
|
if (k === "class") {
|
||
|
updateClass.call(this, "", oldAttrs[k]);
|
||
|
}
|
||
|
else {
|
||
|
removeAttribute.call(this, k);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
for (let k in attrs) {
|
||
|
const val = attrs[k];
|
||
|
if (val !== oldAttrs[k]) {
|
||
|
if (k === "class") {
|
||
|
updateClass.call(this, val, oldAttrs[k]);
|
||
|
}
|
||
|
else {
|
||
|
setAttribute.call(this, k, val);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function toClassObj(expr) {
|
||
|
const result = {};
|
||
|
switch (typeof expr) {
|
||
|
case "string":
|
||
|
// we transform here a list of classes into an object:
|
||
|
// 'hey you' becomes {hey: true, you: true}
|
||
|
const str = trim.call(expr);
|
||
|
if (!str) {
|
||
|
return {};
|
||
|
}
|
||
|
let words = split.call(str, wordRegexp);
|
||
|
for (let i = 0, l = words.length; i < l; i++) {
|
||
|
result[words[i]] = true;
|
||
|
}
|
||
|
return result;
|
||
|
case "object":
|
||
|
// this is already an object but we may need to split keys:
|
||
|
// {'a': true, 'b c': true} should become {a: true, b: true, c: true}
|
||
|
for (let key in expr) {
|
||
|
const value = expr[key];
|
||
|
if (value) {
|
||
|
key = trim.call(key);
|
||
|
if (!key) {
|
||
|
continue;
|
||
|
}
|
||
|
const words = split.call(key, wordRegexp);
|
||
|
for (let word of words) {
|
||
|
result[word] = value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return result;
|
||
|
case "undefined":
|
||
|
return {};
|
||
|
case "number":
|
||
|
return { [expr]: true };
|
||
|
default:
|
||
|
return { [expr]: true };
|
||
|
}
|
||
|
}
|
||
|
function setClass(val) {
|
||
|
val = val === "" ? {} : toClassObj(val);
|
||
|
// add classes
|
||
|
const cl = this.classList;
|
||
|
for (let c in val) {
|
||
|
tokenListAdd.call(cl, c);
|
||
|
}
|
||
|
}
|
||
|
function updateClass(val, oldVal) {
|
||
|
oldVal = oldVal === "" ? {} : toClassObj(oldVal);
|
||
|
val = val === "" ? {} : toClassObj(val);
|
||
|
const cl = this.classList;
|
||
|
// remove classes
|
||
|
for (let c in oldVal) {
|
||
|
if (!(c in val)) {
|
||
|
tokenListRemove.call(cl, c);
|
||
|
}
|
||
|
}
|
||
|
// add classes
|
||
|
for (let c in val) {
|
||
|
if (!(c in oldVal)) {
|
||
|
tokenListAdd.call(cl, c);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a batched version of a callback so that all calls to it in the same
|
||
|
* microtick will only call the original callback once.
|
||
|
*
|
||
|
* @param callback the callback to batch
|
||
|
* @returns a batched version of the original callback
|
||
|
*/
|
||
|
function batched(callback) {
|
||
|
let scheduled = false;
|
||
|
return async (...args) => {
|
||
|
if (!scheduled) {
|
||
|
scheduled = true;
|
||
|
await Promise.resolve();
|
||
|
scheduled = false;
|
||
|
callback(...args);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Determine whether the given element is contained in its ownerDocument:
|
||
|
* either directly or with a shadow root in between.
|
||
|
*/
|
||
|
function inOwnerDocument(el) {
|
||
|
if (!el) {
|
||
|
return false;
|
||
|
}
|
||
|
if (el.ownerDocument.contains(el)) {
|
||
|
return true;
|
||
|
}
|
||
|
const rootNode = el.getRootNode();
|
||
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||
|
}
|
||
|
function validateTarget(target) {
|
||
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||
|
const document = target && target.ownerDocument;
|
||
|
if (document) {
|
||
|
const HTMLElement = document.defaultView.HTMLElement;
|
||
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||
|
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
||
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
|
||
|
}
|
||
|
class EventBus extends EventTarget {
|
||
|
trigger(name, payload) {
|
||
|
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
|
||
|
}
|
||
|
}
|
||
|
function whenReady(fn) {
|
||
|
return new Promise(function (resolve) {
|
||
|
if (document.readyState !== "loading") {
|
||
|
resolve(true);
|
||
|
}
|
||
|
else {
|
||
|
document.addEventListener("DOMContentLoaded", resolve, false);
|
||
|
}
|
||
|
}).then(fn || function () { });
|
||
|
}
|
||
|
async function loadFile(url) {
|
||
|
const result = await fetch(url);
|
||
|
if (!result.ok) {
|
||
|
throw new OwlError("Error while fetching xml templates");
|
||
|
}
|
||
|
return await result.text();
|
||
|
}
|
||
|
/*
|
||
|
* This class just transports the fact that a string is safe
|
||
|
* to be injected as HTML. Overriding a JS primitive is quite painful though
|
||
|
* so we need to redfine toString and valueOf.
|
||
|
*/
|
||
|
class Markup extends String {
|
||
|
}
|
||
|
/*
|
||
|
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||
|
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||
|
*/
|
||
|
function markup(value) {
|
||
|
return new Markup(value);
|
||
|
}
|
||
|
|
||
|
function createEventHandler(rawEvent) {
|
||
|
const eventName = rawEvent.split(".")[0];
|
||
|
const capture = rawEvent.includes(".capture");
|
||
|
if (rawEvent.includes(".synthetic")) {
|
||
|
return createSyntheticHandler(eventName, capture);
|
||
|
}
|
||
|
else {
|
||
|
return createElementHandler(eventName, capture);
|
||
|
}
|
||
|
}
|
||
|
// Native listener
|
||
|
let nextNativeEventId = 1;
|
||
|
function createElementHandler(evName, capture = false) {
|
||
|
let eventKey = `__event__${evName}_${nextNativeEventId++}`;
|
||
|
if (capture) {
|
||
|
eventKey = `${eventKey}_capture`;
|
||
|
}
|
||
|
function listener(ev) {
|
||
|
const currentTarget = ev.currentTarget;
|
||
|
if (!currentTarget || !inOwnerDocument(currentTarget))
|
||
|
return;
|
||
|
const data = currentTarget[eventKey];
|
||
|
if (!data)
|
||
|
return;
|
||
|
config.mainEventHandler(data, ev, currentTarget);
|
||
|
}
|
||
|
function setup(data) {
|
||
|
this[eventKey] = data;
|
||
|
this.addEventListener(evName, listener, { capture });
|
||
|
}
|
||
|
function remove() {
|
||
|
delete this[eventKey];
|
||
|
this.removeEventListener(evName, listener, { capture });
|
||
|
}
|
||
|
function update(data) {
|
||
|
this[eventKey] = data;
|
||
|
}
|
||
|
return { setup, update, remove };
|
||
|
}
|
||
|
// Synthetic handler: a form of event delegation that allows placing only one
|
||
|
// listener per event type.
|
||
|
let nextSyntheticEventId = 1;
|
||
|
function createSyntheticHandler(evName, capture = false) {
|
||
|
let eventKey = `__event__synthetic_${evName}`;
|
||
|
if (capture) {
|
||
|
eventKey = `${eventKey}_capture`;
|
||
|
}
|
||
|
setupSyntheticEvent(evName, eventKey, capture);
|
||
|
const currentId = nextSyntheticEventId++;
|
||
|
function setup(data) {
|
||
|
const _data = this[eventKey] || {};
|
||
|
_data[currentId] = data;
|
||
|
this[eventKey] = _data;
|
||
|
}
|
||
|
function remove() {
|
||
|
delete this[eventKey];
|
||
|
}
|
||
|
return { setup, update: setup, remove };
|
||
|
}
|
||
|
function nativeToSyntheticEvent(eventKey, event) {
|
||
|
let dom = event.target;
|
||
|
while (dom !== null) {
|
||
|
const _data = dom[eventKey];
|
||
|
if (_data) {
|
||
|
for (const data of Object.values(_data)) {
|
||
|
const stopped = config.mainEventHandler(data, event, dom);
|
||
|
if (stopped)
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
dom = dom.parentNode;
|
||
|
}
|
||
|
}
|
||
|
const CONFIGURED_SYNTHETIC_EVENTS = {};
|
||
|
function setupSyntheticEvent(evName, eventKey, capture = false) {
|
||
|
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
|
||
|
return;
|
||
|
}
|
||
|
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {
|
||
|
capture,
|
||
|
});
|
||
|
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
|
||
|
}
|
||
|
|
||
|
const getDescriptor$3 = (o, p) => Object.getOwnPropertyDescriptor(o, p);
|
||
|
const nodeProto$4 = Node.prototype;
|
||
|
const nodeInsertBefore$3 = nodeProto$4.insertBefore;
|
||
|
const nodeSetTextContent$1 = getDescriptor$3(nodeProto$4, "textContent").set;
|
||
|
const nodeRemoveChild$3 = nodeProto$4.removeChild;
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Multi NODE
|
||
|
// -----------------------------------------------------------------------------
|
||
|
class VMulti {
|
||
|
constructor(children) {
|
||
|
this.children = children;
|
||
|
}
|
||
|
mount(parent, afterNode) {
|
||
|
const children = this.children;
|
||
|
const l = children.length;
|
||
|
const anchors = new Array(l);
|
||
|
for (let i = 0; i < l; i++) {
|
||
|
let child = children[i];
|
||
|
if (child) {
|
||
|
child.mount(parent, afterNode);
|
||
|
}
|
||
|
else {
|
||
|
const childAnchor = document.createTextNode("");
|
||
|
anchors[i] = childAnchor;
|
||
|
nodeInsertBefore$3.call(parent, childAnchor, afterNode);
|
||
|
}
|
||
|
}
|
||
|
this.anchors = anchors;
|
||
|
this.parentEl = parent;
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent = this.parentEl) {
|
||
|
this.parentEl = parent;
|
||
|
const children = this.children;
|
||
|
const anchors = this.anchors;
|
||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||
|
let child = children[i];
|
||
|
if (child) {
|
||
|
child.moveBeforeDOMNode(node, parent);
|
||
|
}
|
||
|
else {
|
||
|
const anchor = anchors[i];
|
||
|
nodeInsertBefore$3.call(parent, anchor, node);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
if (other) {
|
||
|
const next = other.children[0];
|
||
|
afterNode = (next ? next.firstNode() : other.anchors[0]) || null;
|
||
|
}
|
||
|
const children = this.children;
|
||
|
const parent = this.parentEl;
|
||
|
const anchors = this.anchors;
|
||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||
|
let child = children[i];
|
||
|
if (child) {
|
||
|
child.moveBeforeVNode(null, afterNode);
|
||
|
}
|
||
|
else {
|
||
|
const anchor = anchors[i];
|
||
|
nodeInsertBefore$3.call(parent, anchor, afterNode);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
patch(other, withBeforeRemove) {
|
||
|
if (this === other) {
|
||
|
return;
|
||
|
}
|
||
|
const children1 = this.children;
|
||
|
const children2 = other.children;
|
||
|
const anchors = this.anchors;
|
||
|
const parentEl = this.parentEl;
|
||
|
for (let i = 0, l = children1.length; i < l; i++) {
|
||
|
const vn1 = children1[i];
|
||
|
const vn2 = children2[i];
|
||
|
if (vn1) {
|
||
|
if (vn2) {
|
||
|
vn1.patch(vn2, withBeforeRemove);
|
||
|
}
|
||
|
else {
|
||
|
const afterNode = vn1.firstNode();
|
||
|
const anchor = document.createTextNode("");
|
||
|
anchors[i] = anchor;
|
||
|
nodeInsertBefore$3.call(parentEl, anchor, afterNode);
|
||
|
if (withBeforeRemove) {
|
||
|
vn1.beforeRemove();
|
||
|
}
|
||
|
vn1.remove();
|
||
|
children1[i] = undefined;
|
||
|
}
|
||
|
}
|
||
|
else if (vn2) {
|
||
|
children1[i] = vn2;
|
||
|
const anchor = anchors[i];
|
||
|
vn2.mount(parentEl, anchor);
|
||
|
nodeRemoveChild$3.call(parentEl, anchor);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
beforeRemove() {
|
||
|
const children = this.children;
|
||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||
|
const child = children[i];
|
||
|
if (child) {
|
||
|
child.beforeRemove();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
remove() {
|
||
|
const parentEl = this.parentEl;
|
||
|
if (this.isOnlyChild) {
|
||
|
nodeSetTextContent$1.call(parentEl, "");
|
||
|
}
|
||
|
else {
|
||
|
const children = this.children;
|
||
|
const anchors = this.anchors;
|
||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||
|
const child = children[i];
|
||
|
if (child) {
|
||
|
child.remove();
|
||
|
}
|
||
|
else {
|
||
|
nodeRemoveChild$3.call(parentEl, anchors[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
firstNode() {
|
||
|
const child = this.children[0];
|
||
|
return child ? child.firstNode() : this.anchors[0];
|
||
|
}
|
||
|
toString() {
|
||
|
return this.children.map((c) => (c ? c.toString() : "")).join("");
|
||
|
}
|
||
|
}
|
||
|
function multi(children) {
|
||
|
return new VMulti(children);
|
||
|
}
|
||
|
|
||
|
const getDescriptor$2 = (o, p) => Object.getOwnPropertyDescriptor(o, p);
|
||
|
const nodeProto$3 = Node.prototype;
|
||
|
const characterDataProto$1 = CharacterData.prototype;
|
||
|
const nodeInsertBefore$2 = nodeProto$3.insertBefore;
|
||
|
const characterDataSetData$1 = getDescriptor$2(characterDataProto$1, "data").set;
|
||
|
const nodeRemoveChild$2 = nodeProto$3.removeChild;
|
||
|
class VSimpleNode {
|
||
|
constructor(text) {
|
||
|
this.text = text;
|
||
|
}
|
||
|
mountNode(node, parent, afterNode) {
|
||
|
this.parentEl = parent;
|
||
|
nodeInsertBefore$2.call(parent, node, afterNode);
|
||
|
this.el = node;
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent = this.parentEl) {
|
||
|
this.parentEl = parent;
|
||
|
nodeInsertBefore$2.call(parent, this.el, node);
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
nodeInsertBefore$2.call(this.parentEl, this.el, other ? other.el : afterNode);
|
||
|
}
|
||
|
beforeRemove() { }
|
||
|
remove() {
|
||
|
nodeRemoveChild$2.call(this.parentEl, this.el);
|
||
|
}
|
||
|
firstNode() {
|
||
|
return this.el;
|
||
|
}
|
||
|
toString() {
|
||
|
return this.text;
|
||
|
}
|
||
|
}
|
||
|
class VText$1 extends VSimpleNode {
|
||
|
mount(parent, afterNode) {
|
||
|
this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);
|
||
|
}
|
||
|
patch(other) {
|
||
|
const text2 = other.text;
|
||
|
if (this.text !== text2) {
|
||
|
characterDataSetData$1.call(this.el, toText(text2));
|
||
|
this.text = text2;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
class VComment extends VSimpleNode {
|
||
|
mount(parent, afterNode) {
|
||
|
this.mountNode(document.createComment(toText(this.text)), parent, afterNode);
|
||
|
}
|
||
|
patch() { }
|
||
|
}
|
||
|
function text(str) {
|
||
|
return new VText$1(str);
|
||
|
}
|
||
|
function comment(str) {
|
||
|
return new VComment(str);
|
||
|
}
|
||
|
function toText(value) {
|
||
|
switch (typeof value) {
|
||
|
case "string":
|
||
|
return value;
|
||
|
case "number":
|
||
|
return String(value);
|
||
|
case "boolean":
|
||
|
return value ? "true" : "false";
|
||
|
default:
|
||
|
return value || "";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const getDescriptor$1 = (o, p) => Object.getOwnPropertyDescriptor(o, p);
|
||
|
const nodeProto$2 = Node.prototype;
|
||
|
const elementProto = Element.prototype;
|
||
|
const characterDataProto = CharacterData.prototype;
|
||
|
const characterDataSetData = getDescriptor$1(characterDataProto, "data").set;
|
||
|
const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get;
|
||
|
const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get;
|
||
|
const NO_OP = () => { };
|
||
|
function makePropSetter(name) {
|
||
|
return function setProp(value) {
|
||
|
// support 0, fallback to empty string for other falsy values
|
||
|
this[name] = value === 0 ? 0 : value ? value.valueOf() : "";
|
||
|
};
|
||
|
}
|
||
|
const cache$1 = {};
|
||
|
/**
|
||
|
* Compiling blocks is a multi-step process:
|
||
|
*
|
||
|
* 1. build an IntermediateTree from the HTML element. This intermediate tree
|
||
|
* is a binary tree structure that encode dynamic info sub nodes, and the
|
||
|
* path required to reach them
|
||
|
* 2. process the tree to build a block context, which is an object that aggregate
|
||
|
* all dynamic info in a list, and also, all ref indexes.
|
||
|
* 3. process the context to build appropriate builder/setter functions
|
||
|
* 4. make a dynamic block class, which will efficiently collect references and
|
||
|
* create/update dynamic locations/children
|
||
|
*
|
||
|
* @param str
|
||
|
* @returns a new block type, that can build concrete blocks
|
||
|
*/
|
||
|
function createBlock(str) {
|
||
|
if (str in cache$1) {
|
||
|
return cache$1[str];
|
||
|
}
|
||
|
// step 0: prepare html base element
|
||
|
const doc = new DOMParser().parseFromString(`<t>${str}</t>`, "text/xml");
|
||
|
const node = doc.firstChild.firstChild;
|
||
|
if (config.shouldNormalizeDom) {
|
||
|
normalizeNode(node);
|
||
|
}
|
||
|
// step 1: prepare intermediate tree
|
||
|
const tree = buildTree(node);
|
||
|
// step 2: prepare block context
|
||
|
const context = buildContext(tree);
|
||
|
// step 3: build the final block class
|
||
|
const template = tree.el;
|
||
|
const Block = buildBlock(template, context);
|
||
|
cache$1[str] = Block;
|
||
|
return Block;
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Helper
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function normalizeNode(node) {
|
||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||
|
if (!/\S/.test(node.textContent)) {
|
||
|
node.remove();
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||
|
if (node.tagName === "pre") {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
for (let i = node.childNodes.length - 1; i >= 0; --i) {
|
||
|
normalizeNode(node.childNodes.item(i));
|
||
|
}
|
||
|
}
|
||
|
function buildTree(node, parent = null, domParentTree = null) {
|
||
|
switch (node.nodeType) {
|
||
|
case Node.ELEMENT_NODE: {
|
||
|
// HTMLElement
|
||
|
let currentNS = domParentTree && domParentTree.currentNS;
|
||
|
const tagName = node.tagName;
|
||
|
let el = undefined;
|
||
|
const info = [];
|
||
|
if (tagName.startsWith("block-text-")) {
|
||
|
const index = parseInt(tagName.slice(11), 10);
|
||
|
info.push({ type: "text", idx: index });
|
||
|
el = document.createTextNode("");
|
||
|
}
|
||
|
if (tagName.startsWith("block-child-")) {
|
||
|
if (!domParentTree.isRef) {
|
||
|
addRef(domParentTree);
|
||
|
}
|
||
|
const index = parseInt(tagName.slice(12), 10);
|
||
|
info.push({ type: "child", idx: index });
|
||
|
el = document.createTextNode("");
|
||
|
}
|
||
|
currentNS || (currentNS = node.namespaceURI);
|
||
|
if (!el) {
|
||
|
el = currentNS
|
||
|
? document.createElementNS(currentNS, tagName)
|
||
|
: document.createElement(tagName);
|
||
|
}
|
||
|
if (el instanceof Element) {
|
||
|
if (!domParentTree) {
|
||
|
// some html elements may have side effects when setting their attributes.
|
||
|
// For example, setting the src attribute of an <img/> will trigger a
|
||
|
// request to get the corresponding image. This is something that we
|
||
|
// don't want at compile time. We avoid that by putting the content of
|
||
|
// the block in a <template/> element
|
||
|
const fragment = document.createElement("template").content;
|
||
|
fragment.appendChild(el);
|
||
|
}
|
||
|
const attrs = node.attributes;
|
||
|
for (let i = 0; i < attrs.length; i++) {
|
||
|
const attrName = attrs[i].name;
|
||
|
const attrValue = attrs[i].value;
|
||
|
if (attrName.startsWith("block-handler-")) {
|
||
|
const idx = parseInt(attrName.slice(14), 10);
|
||
|
info.push({
|
||
|
type: "handler",
|
||
|
idx,
|
||
|
event: attrValue,
|
||
|
});
|
||
|
}
|
||
|
else if (attrName.startsWith("block-attribute-")) {
|
||
|
const idx = parseInt(attrName.slice(16), 10);
|
||
|
info.push({
|
||
|
type: "attribute",
|
||
|
idx,
|
||
|
name: attrValue,
|
||
|
tag: tagName,
|
||
|
});
|
||
|
}
|
||
|
else if (attrName.startsWith("block-property-")) {
|
||
|
const idx = parseInt(attrName.slice(15), 10);
|
||
|
info.push({
|
||
|
type: "property",
|
||
|
idx,
|
||
|
name: attrValue,
|
||
|
tag: tagName,
|
||
|
});
|
||
|
}
|
||
|
else if (attrName === "block-attributes") {
|
||
|
info.push({
|
||
|
type: "attributes",
|
||
|
idx: parseInt(attrValue, 10),
|
||
|
});
|
||
|
}
|
||
|
else if (attrName === "block-ref") {
|
||
|
info.push({
|
||
|
type: "ref",
|
||
|
idx: parseInt(attrValue, 10),
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
el.setAttribute(attrs[i].name, attrValue);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const tree = {
|
||
|
parent,
|
||
|
firstChild: null,
|
||
|
nextSibling: null,
|
||
|
el,
|
||
|
info,
|
||
|
refN: 0,
|
||
|
currentNS,
|
||
|
};
|
||
|
if (node.firstChild) {
|
||
|
const childNode = node.childNodes[0];
|
||
|
if (node.childNodes.length === 1 &&
|
||
|
childNode.nodeType === Node.ELEMENT_NODE &&
|
||
|
childNode.tagName.startsWith("block-child-")) {
|
||
|
const tagName = childNode.tagName;
|
||
|
const index = parseInt(tagName.slice(12), 10);
|
||
|
info.push({ idx: index, type: "child", isOnlyChild: true });
|
||
|
}
|
||
|
else {
|
||
|
tree.firstChild = buildTree(node.firstChild, tree, tree);
|
||
|
el.appendChild(tree.firstChild.el);
|
||
|
let curNode = node.firstChild;
|
||
|
let curTree = tree.firstChild;
|
||
|
while ((curNode = curNode.nextSibling)) {
|
||
|
curTree.nextSibling = buildTree(curNode, curTree, tree);
|
||
|
el.appendChild(curTree.nextSibling.el);
|
||
|
curTree = curTree.nextSibling;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (tree.info.length) {
|
||
|
addRef(tree);
|
||
|
}
|
||
|
return tree;
|
||
|
}
|
||
|
case Node.TEXT_NODE:
|
||
|
case Node.COMMENT_NODE: {
|
||
|
// text node or comment node
|
||
|
const el = node.nodeType === Node.TEXT_NODE
|
||
|
? document.createTextNode(node.textContent)
|
||
|
: document.createComment(node.textContent);
|
||
|
return {
|
||
|
parent: parent,
|
||
|
firstChild: null,
|
||
|
nextSibling: null,
|
||
|
el,
|
||
|
info: [],
|
||
|
refN: 0,
|
||
|
currentNS: null,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
throw new OwlError("boom");
|
||
|
}
|
||
|
function addRef(tree) {
|
||
|
tree.isRef = true;
|
||
|
do {
|
||
|
tree.refN++;
|
||
|
} while ((tree = tree.parent));
|
||
|
}
|
||
|
function parentTree(tree) {
|
||
|
let parent = tree.parent;
|
||
|
while (parent && parent.nextSibling === tree) {
|
||
|
tree = parent;
|
||
|
parent = parent.parent;
|
||
|
}
|
||
|
return parent;
|
||
|
}
|
||
|
function buildContext(tree, ctx, fromIdx) {
|
||
|
if (!ctx) {
|
||
|
const children = new Array(tree.info.filter((v) => v.type === "child").length);
|
||
|
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };
|
||
|
fromIdx = 0;
|
||
|
}
|
||
|
if (tree.refN) {
|
||
|
const initialIdx = fromIdx;
|
||
|
const isRef = tree.isRef;
|
||
|
const firstChild = tree.firstChild ? tree.firstChild.refN : 0;
|
||
|
const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;
|
||
|
//node
|
||
|
if (isRef) {
|
||
|
for (let info of tree.info) {
|
||
|
info.refIdx = initialIdx;
|
||
|
}
|
||
|
tree.refIdx = initialIdx;
|
||
|
updateCtx(ctx, tree);
|
||
|
fromIdx++;
|
||
|
}
|
||
|
// right
|
||
|
if (nextSibling) {
|
||
|
const idx = fromIdx + firstChild;
|
||
|
ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });
|
||
|
buildContext(tree.nextSibling, ctx, idx);
|
||
|
}
|
||
|
// left
|
||
|
if (firstChild) {
|
||
|
ctx.collectors.push({ idx: fromIdx, prevIdx: initialIdx, getVal: nodeGetFirstChild });
|
||
|
buildContext(tree.firstChild, ctx, fromIdx);
|
||
|
}
|
||
|
}
|
||
|
return ctx;
|
||
|
}
|
||
|
function updateCtx(ctx, tree) {
|
||
|
for (let info of tree.info) {
|
||
|
switch (info.type) {
|
||
|
case "text":
|
||
|
ctx.locations.push({
|
||
|
idx: info.idx,
|
||
|
refIdx: info.refIdx,
|
||
|
setData: setText,
|
||
|
updateData: setText,
|
||
|
});
|
||
|
break;
|
||
|
case "child":
|
||
|
if (info.isOnlyChild) {
|
||
|
// tree is the parentnode here
|
||
|
ctx.children[info.idx] = {
|
||
|
parentRefIdx: info.refIdx,
|
||
|
isOnlyChild: true,
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
// tree is the anchor text node
|
||
|
ctx.children[info.idx] = {
|
||
|
parentRefIdx: parentTree(tree).refIdx,
|
||
|
afterRefIdx: info.refIdx,
|
||
|
};
|
||
|
}
|
||
|
break;
|
||
|
case "property": {
|
||
|
const refIdx = info.refIdx;
|
||
|
const setProp = makePropSetter(info.name);
|
||
|
ctx.locations.push({
|
||
|
idx: info.idx,
|
||
|
refIdx,
|
||
|
setData: setProp,
|
||
|
updateData: setProp,
|
||
|
});
|
||
|
break;
|
||
|
}
|
||
|
case "attribute": {
|
||
|
const refIdx = info.refIdx;
|
||
|
let updater;
|
||
|
let setter;
|
||
|
if (info.name === "class") {
|
||
|
setter = setClass;
|
||
|
updater = updateClass;
|
||
|
}
|
||
|
else {
|
||
|
setter = createAttrUpdater(info.name);
|
||
|
updater = setter;
|
||
|
}
|
||
|
ctx.locations.push({
|
||
|
idx: info.idx,
|
||
|
refIdx,
|
||
|
setData: setter,
|
||
|
updateData: updater,
|
||
|
});
|
||
|
break;
|
||
|
}
|
||
|
case "attributes":
|
||
|
ctx.locations.push({
|
||
|
idx: info.idx,
|
||
|
refIdx: info.refIdx,
|
||
|
setData: attrsSetter,
|
||
|
updateData: attrsUpdater,
|
||
|
});
|
||
|
break;
|
||
|
case "handler": {
|
||
|
const { setup, update } = createEventHandler(info.event);
|
||
|
ctx.locations.push({
|
||
|
idx: info.idx,
|
||
|
refIdx: info.refIdx,
|
||
|
setData: setup,
|
||
|
updateData: update,
|
||
|
});
|
||
|
break;
|
||
|
}
|
||
|
case "ref":
|
||
|
const index = ctx.cbRefs.push(info.idx) - 1;
|
||
|
ctx.locations.push({
|
||
|
idx: info.idx,
|
||
|
refIdx: info.refIdx,
|
||
|
setData: makeRefSetter(index, ctx.refList),
|
||
|
updateData: NO_OP,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// building the concrete block class
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function buildBlock(template, ctx) {
|
||
|
let B = createBlockClass(template, ctx);
|
||
|
if (ctx.cbRefs.length) {
|
||
|
const cbRefs = ctx.cbRefs;
|
||
|
const refList = ctx.refList;
|
||
|
let cbRefsNumber = cbRefs.length;
|
||
|
B = class extends B {
|
||
|
mount(parent, afterNode) {
|
||
|
refList.push(new Array(cbRefsNumber));
|
||
|
super.mount(parent, afterNode);
|
||
|
for (let cbRef of refList.pop()) {
|
||
|
cbRef();
|
||
|
}
|
||
|
}
|
||
|
remove() {
|
||
|
super.remove();
|
||
|
for (let cbRef of cbRefs) {
|
||
|
let fn = this.data[cbRef];
|
||
|
fn(null);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
if (ctx.children.length) {
|
||
|
B = class extends B {
|
||
|
constructor(data, children) {
|
||
|
super(data);
|
||
|
this.children = children;
|
||
|
}
|
||
|
};
|
||
|
B.prototype.beforeRemove = VMulti.prototype.beforeRemove;
|
||
|
return (data, children = []) => new B(data, children);
|
||
|
}
|
||
|
return (data) => new B(data);
|
||
|
}
|
||
|
function createBlockClass(template, ctx) {
|
||
|
const { refN, collectors, children } = ctx;
|
||
|
const colN = collectors.length;
|
||
|
ctx.locations.sort((a, b) => a.idx - b.idx);
|
||
|
const locations = ctx.locations.map((loc) => ({
|
||
|
refIdx: loc.refIdx,
|
||
|
setData: loc.setData,
|
||
|
updateData: loc.updateData,
|
||
|
}));
|
||
|
const locN = locations.length;
|
||
|
const childN = children.length;
|
||
|
const childrenLocs = children;
|
||
|
const isDynamic = refN > 0;
|
||
|
// these values are defined here to make them faster to lookup in the class
|
||
|
// block scope
|
||
|
const nodeCloneNode = nodeProto$2.cloneNode;
|
||
|
const nodeInsertBefore = nodeProto$2.insertBefore;
|
||
|
const elementRemove = elementProto.remove;
|
||
|
class Block {
|
||
|
constructor(data) {
|
||
|
this.data = data;
|
||
|
}
|
||
|
beforeRemove() { }
|
||
|
remove() {
|
||
|
elementRemove.call(this.el);
|
||
|
}
|
||
|
firstNode() {
|
||
|
return this.el;
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent = this.parentEl) {
|
||
|
this.parentEl = parent;
|
||
|
nodeInsertBefore.call(parent, this.el, node);
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
nodeInsertBefore.call(this.parentEl, this.el, other ? other.el : afterNode);
|
||
|
}
|
||
|
toString() {
|
||
|
const div = document.createElement("div");
|
||
|
this.mount(div, null);
|
||
|
return div.innerHTML;
|
||
|
}
|
||
|
mount(parent, afterNode) {
|
||
|
const el = nodeCloneNode.call(template, true);
|
||
|
nodeInsertBefore.call(parent, el, afterNode);
|
||
|
this.el = el;
|
||
|
this.parentEl = parent;
|
||
|
}
|
||
|
patch(other, withBeforeRemove) { }
|
||
|
}
|
||
|
if (isDynamic) {
|
||
|
Block.prototype.mount = function mount(parent, afterNode) {
|
||
|
const el = nodeCloneNode.call(template, true);
|
||
|
// collecting references
|
||
|
const refs = new Array(refN);
|
||
|
this.refs = refs;
|
||
|
refs[0] = el;
|
||
|
for (let i = 0; i < colN; i++) {
|
||
|
const w = collectors[i];
|
||
|
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
|
||
|
}
|
||
|
// applying data to all update points
|
||
|
if (locN) {
|
||
|
const data = this.data;
|
||
|
for (let i = 0; i < locN; i++) {
|
||
|
const loc = locations[i];
|
||
|
loc.setData.call(refs[loc.refIdx], data[i]);
|
||
|
}
|
||
|
}
|
||
|
nodeInsertBefore.call(parent, el, afterNode);
|
||
|
// preparing all children
|
||
|
if (childN) {
|
||
|
const children = this.children;
|
||
|
for (let i = 0; i < childN; i++) {
|
||
|
const child = children[i];
|
||
|
if (child) {
|
||
|
const loc = childrenLocs[i];
|
||
|
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
|
||
|
child.isOnlyChild = loc.isOnlyChild;
|
||
|
child.mount(refs[loc.parentRefIdx], afterNode);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.el = el;
|
||
|
this.parentEl = parent;
|
||
|
};
|
||
|
Block.prototype.patch = function patch(other, withBeforeRemove) {
|
||
|
if (this === other) {
|
||
|
return;
|
||
|
}
|
||
|
const refs = this.refs;
|
||
|
// update texts/attributes/
|
||
|
if (locN) {
|
||
|
const data1 = this.data;
|
||
|
const data2 = other.data;
|
||
|
for (let i = 0; i < locN; i++) {
|
||
|
const val1 = data1[i];
|
||
|
const val2 = data2[i];
|
||
|
if (val1 !== val2) {
|
||
|
const loc = locations[i];
|
||
|
loc.updateData.call(refs[loc.refIdx], val2, val1);
|
||
|
}
|
||
|
}
|
||
|
this.data = data2;
|
||
|
}
|
||
|
// update children
|
||
|
if (childN) {
|
||
|
let children1 = this.children;
|
||
|
const children2 = other.children;
|
||
|
for (let i = 0; i < childN; i++) {
|
||
|
const child1 = children1[i];
|
||
|
const child2 = children2[i];
|
||
|
if (child1) {
|
||
|
if (child2) {
|
||
|
child1.patch(child2, withBeforeRemove);
|
||
|
}
|
||
|
else {
|
||
|
if (withBeforeRemove) {
|
||
|
child1.beforeRemove();
|
||
|
}
|
||
|
child1.remove();
|
||
|
children1[i] = undefined;
|
||
|
}
|
||
|
}
|
||
|
else if (child2) {
|
||
|
const loc = childrenLocs[i];
|
||
|
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
|
||
|
child2.mount(refs[loc.parentRefIdx], afterNode);
|
||
|
children1[i] = child2;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
return Block;
|
||
|
}
|
||
|
function setText(value) {
|
||
|
characterDataSetData.call(this, toText(value));
|
||
|
}
|
||
|
function makeRefSetter(index, refs) {
|
||
|
return function setRef(fn) {
|
||
|
refs[refs.length - 1][index] = () => fn(this);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const getDescriptor = (o, p) => Object.getOwnPropertyDescriptor(o, p);
|
||
|
const nodeProto$1 = Node.prototype;
|
||
|
const nodeInsertBefore$1 = nodeProto$1.insertBefore;
|
||
|
const nodeAppendChild = nodeProto$1.appendChild;
|
||
|
const nodeRemoveChild$1 = nodeProto$1.removeChild;
|
||
|
const nodeSetTextContent = getDescriptor(nodeProto$1, "textContent").set;
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// List Node
|
||
|
// -----------------------------------------------------------------------------
|
||
|
class VList {
|
||
|
constructor(children) {
|
||
|
this.children = children;
|
||
|
}
|
||
|
mount(parent, afterNode) {
|
||
|
const children = this.children;
|
||
|
const _anchor = document.createTextNode("");
|
||
|
this.anchor = _anchor;
|
||
|
nodeInsertBefore$1.call(parent, _anchor, afterNode);
|
||
|
const l = children.length;
|
||
|
if (l) {
|
||
|
const mount = children[0].mount;
|
||
|
for (let i = 0; i < l; i++) {
|
||
|
mount.call(children[i], parent, _anchor);
|
||
|
}
|
||
|
}
|
||
|
this.parentEl = parent;
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent = this.parentEl) {
|
||
|
this.parentEl = parent;
|
||
|
const children = this.children;
|
||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||
|
children[i].moveBeforeDOMNode(node, parent);
|
||
|
}
|
||
|
parent.insertBefore(this.anchor, node);
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
if (other) {
|
||
|
const next = other.children[0];
|
||
|
afterNode = (next ? next.firstNode() : other.anchor) || null;
|
||
|
}
|
||
|
const children = this.children;
|
||
|
for (let i = 0, l = children.length; i < l; i++) {
|
||
|
children[i].moveBeforeVNode(null, afterNode);
|
||
|
}
|
||
|
this.parentEl.insertBefore(this.anchor, afterNode);
|
||
|
}
|
||
|
patch(other, withBeforeRemove) {
|
||
|
if (this === other) {
|
||
|
return;
|
||
|
}
|
||
|
const ch1 = this.children;
|
||
|
const ch2 = other.children;
|
||
|
if (ch2.length === 0 && ch1.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
this.children = ch2;
|
||
|
const proto = ch2[0] || ch1[0];
|
||
|
const { mount: cMount, patch: cPatch, remove: cRemove, beforeRemove, moveBeforeVNode: cMoveBefore, firstNode: cFirstNode, } = proto;
|
||
|
const _anchor = this.anchor;
|
||
|
const isOnlyChild = this.isOnlyChild;
|
||
|
const parent = this.parentEl;
|
||
|
// fast path: no new child => only remove
|
||
|
if (ch2.length === 0 && isOnlyChild) {
|
||
|
if (withBeforeRemove) {
|
||
|
for (let i = 0, l = ch1.length; i < l; i++) {
|
||
|
beforeRemove.call(ch1[i]);
|
||
|
}
|
||
|
}
|
||
|
nodeSetTextContent.call(parent, "");
|
||
|
nodeAppendChild.call(parent, _anchor);
|
||
|
return;
|
||
|
}
|
||
|
let startIdx1 = 0;
|
||
|
let startIdx2 = 0;
|
||
|
let startVn1 = ch1[0];
|
||
|
let startVn2 = ch2[0];
|
||
|
let endIdx1 = ch1.length - 1;
|
||
|
let endIdx2 = ch2.length - 1;
|
||
|
let endVn1 = ch1[endIdx1];
|
||
|
let endVn2 = ch2[endIdx2];
|
||
|
let mapping = undefined;
|
||
|
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
|
||
|
// -------------------------------------------------------------------
|
||
|
if (startVn1 === null) {
|
||
|
startVn1 = ch1[++startIdx1];
|
||
|
continue;
|
||
|
}
|
||
|
// -------------------------------------------------------------------
|
||
|
if (endVn1 === null) {
|
||
|
endVn1 = ch1[--endIdx1];
|
||
|
continue;
|
||
|
}
|
||
|
// -------------------------------------------------------------------
|
||
|
let startKey1 = startVn1.key;
|
||
|
let startKey2 = startVn2.key;
|
||
|
if (startKey1 === startKey2) {
|
||
|
cPatch.call(startVn1, startVn2, withBeforeRemove);
|
||
|
ch2[startIdx2] = startVn1;
|
||
|
startVn1 = ch1[++startIdx1];
|
||
|
startVn2 = ch2[++startIdx2];
|
||
|
continue;
|
||
|
}
|
||
|
// -------------------------------------------------------------------
|
||
|
let endKey1 = endVn1.key;
|
||
|
let endKey2 = endVn2.key;
|
||
|
if (endKey1 === endKey2) {
|
||
|
cPatch.call(endVn1, endVn2, withBeforeRemove);
|
||
|
ch2[endIdx2] = endVn1;
|
||
|
endVn1 = ch1[--endIdx1];
|
||
|
endVn2 = ch2[--endIdx2];
|
||
|
continue;
|
||
|
}
|
||
|
// -------------------------------------------------------------------
|
||
|
if (startKey1 === endKey2) {
|
||
|
// bnode moved right
|
||
|
cPatch.call(startVn1, endVn2, withBeforeRemove);
|
||
|
ch2[endIdx2] = startVn1;
|
||
|
const nextChild = ch2[endIdx2 + 1];
|
||
|
cMoveBefore.call(startVn1, nextChild, _anchor);
|
||
|
startVn1 = ch1[++startIdx1];
|
||
|
endVn2 = ch2[--endIdx2];
|
||
|
continue;
|
||
|
}
|
||
|
// -------------------------------------------------------------------
|
||
|
if (endKey1 === startKey2) {
|
||
|
// bnode moved left
|
||
|
cPatch.call(endVn1, startVn2, withBeforeRemove);
|
||
|
ch2[startIdx2] = endVn1;
|
||
|
const nextChild = ch1[startIdx1];
|
||
|
cMoveBefore.call(endVn1, nextChild, _anchor);
|
||
|
endVn1 = ch1[--endIdx1];
|
||
|
startVn2 = ch2[++startIdx2];
|
||
|
continue;
|
||
|
}
|
||
|
// -------------------------------------------------------------------
|
||
|
mapping = mapping || createMapping(ch1, startIdx1, endIdx1);
|
||
|
let idxInOld = mapping[startKey2];
|
||
|
if (idxInOld === undefined) {
|
||
|
cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);
|
||
|
}
|
||
|
else {
|
||
|
const elmToMove = ch1[idxInOld];
|
||
|
cMoveBefore.call(elmToMove, startVn1, null);
|
||
|
cPatch.call(elmToMove, startVn2, withBeforeRemove);
|
||
|
ch2[startIdx2] = elmToMove;
|
||
|
ch1[idxInOld] = null;
|
||
|
}
|
||
|
startVn2 = ch2[++startIdx2];
|
||
|
}
|
||
|
// ---------------------------------------------------------------------
|
||
|
if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {
|
||
|
if (startIdx1 > endIdx1) {
|
||
|
const nextChild = ch2[endIdx2 + 1];
|
||
|
const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;
|
||
|
for (let i = startIdx2; i <= endIdx2; i++) {
|
||
|
cMount.call(ch2[i], parent, anchor);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
for (let i = startIdx1; i <= endIdx1; i++) {
|
||
|
let ch = ch1[i];
|
||
|
if (ch) {
|
||
|
if (withBeforeRemove) {
|
||
|
beforeRemove.call(ch);
|
||
|
}
|
||
|
cRemove.call(ch);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
beforeRemove() {
|
||
|
const children = this.children;
|
||
|
const l = children.length;
|
||
|
if (l) {
|
||
|
const beforeRemove = children[0].beforeRemove;
|
||
|
for (let i = 0; i < l; i++) {
|
||
|
beforeRemove.call(children[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
remove() {
|
||
|
const { parentEl, anchor } = this;
|
||
|
if (this.isOnlyChild) {
|
||
|
nodeSetTextContent.call(parentEl, "");
|
||
|
}
|
||
|
else {
|
||
|
const children = this.children;
|
||
|
const l = children.length;
|
||
|
if (l) {
|
||
|
const remove = children[0].remove;
|
||
|
for (let i = 0; i < l; i++) {
|
||
|
remove.call(children[i]);
|
||
|
}
|
||
|
}
|
||
|
nodeRemoveChild$1.call(parentEl, anchor);
|
||
|
}
|
||
|
}
|
||
|
firstNode() {
|
||
|
const child = this.children[0];
|
||
|
return child ? child.firstNode() : undefined;
|
||
|
}
|
||
|
toString() {
|
||
|
return this.children.map((c) => c.toString()).join("");
|
||
|
}
|
||
|
}
|
||
|
function list(children) {
|
||
|
return new VList(children);
|
||
|
}
|
||
|
function createMapping(ch1, startIdx1, endIdx2) {
|
||
|
let mapping = {};
|
||
|
for (let i = startIdx1; i <= endIdx2; i++) {
|
||
|
mapping[ch1[i].key] = i;
|
||
|
}
|
||
|
return mapping;
|
||
|
}
|
||
|
|
||
|
const nodeProto = Node.prototype;
|
||
|
const nodeInsertBefore = nodeProto.insertBefore;
|
||
|
const nodeRemoveChild = nodeProto.removeChild;
|
||
|
class VHtml {
|
||
|
constructor(html) {
|
||
|
this.content = [];
|
||
|
this.html = html;
|
||
|
}
|
||
|
mount(parent, afterNode) {
|
||
|
this.parentEl = parent;
|
||
|
const template = document.createElement("template");
|
||
|
template.innerHTML = this.html;
|
||
|
this.content = [...template.content.childNodes];
|
||
|
for (let elem of this.content) {
|
||
|
nodeInsertBefore.call(parent, elem, afterNode);
|
||
|
}
|
||
|
if (!this.content.length) {
|
||
|
const textNode = document.createTextNode("");
|
||
|
this.content.push(textNode);
|
||
|
nodeInsertBefore.call(parent, textNode, afterNode);
|
||
|
}
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent = this.parentEl) {
|
||
|
this.parentEl = parent;
|
||
|
for (let elem of this.content) {
|
||
|
nodeInsertBefore.call(parent, elem, node);
|
||
|
}
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
const target = other ? other.content[0] : afterNode;
|
||
|
this.moveBeforeDOMNode(target);
|
||
|
}
|
||
|
patch(other) {
|
||
|
if (this === other) {
|
||
|
return;
|
||
|
}
|
||
|
const html2 = other.html;
|
||
|
if (this.html !== html2) {
|
||
|
const parent = this.parentEl;
|
||
|
// insert new html in front of current
|
||
|
const afterNode = this.content[0];
|
||
|
const template = document.createElement("template");
|
||
|
template.innerHTML = html2;
|
||
|
const content = [...template.content.childNodes];
|
||
|
for (let elem of content) {
|
||
|
nodeInsertBefore.call(parent, elem, afterNode);
|
||
|
}
|
||
|
if (!content.length) {
|
||
|
const textNode = document.createTextNode("");
|
||
|
content.push(textNode);
|
||
|
nodeInsertBefore.call(parent, textNode, afterNode);
|
||
|
}
|
||
|
// remove current content
|
||
|
this.remove();
|
||
|
this.content = content;
|
||
|
this.html = other.html;
|
||
|
}
|
||
|
}
|
||
|
beforeRemove() { }
|
||
|
remove() {
|
||
|
const parent = this.parentEl;
|
||
|
for (let elem of this.content) {
|
||
|
nodeRemoveChild.call(parent, elem);
|
||
|
}
|
||
|
}
|
||
|
firstNode() {
|
||
|
return this.content[0];
|
||
|
}
|
||
|
toString() {
|
||
|
return this.html;
|
||
|
}
|
||
|
}
|
||
|
function html(str) {
|
||
|
return new VHtml(str);
|
||
|
}
|
||
|
|
||
|
function createCatcher(eventsSpec) {
|
||
|
const n = Object.keys(eventsSpec).length;
|
||
|
class VCatcher {
|
||
|
constructor(child, handlers) {
|
||
|
this.handlerFns = [];
|
||
|
this.afterNode = null;
|
||
|
this.child = child;
|
||
|
this.handlerData = handlers;
|
||
|
}
|
||
|
mount(parent, afterNode) {
|
||
|
this.parentEl = parent;
|
||
|
this.child.mount(parent, afterNode);
|
||
|
this.afterNode = document.createTextNode("");
|
||
|
parent.insertBefore(this.afterNode, afterNode);
|
||
|
this.wrapHandlerData();
|
||
|
for (let name in eventsSpec) {
|
||
|
const index = eventsSpec[name];
|
||
|
const handler = createEventHandler(name);
|
||
|
this.handlerFns[index] = handler;
|
||
|
handler.setup.call(parent, this.handlerData[index]);
|
||
|
}
|
||
|
}
|
||
|
wrapHandlerData() {
|
||
|
for (let i = 0; i < n; i++) {
|
||
|
let handler = this.handlerData[i];
|
||
|
// handler = [...mods, fn, comp], so we need to replace second to last elem
|
||
|
let idx = handler.length - 2;
|
||
|
let origFn = handler[idx];
|
||
|
const self = this;
|
||
|
handler[idx] = function (ev) {
|
||
|
const target = ev.target;
|
||
|
let currentNode = self.child.firstNode();
|
||
|
const afterNode = self.afterNode;
|
||
|
while (currentNode && currentNode !== afterNode) {
|
||
|
if (currentNode.contains(target)) {
|
||
|
return origFn.call(this, ev);
|
||
|
}
|
||
|
currentNode = currentNode.nextSibling;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent = this.parentEl) {
|
||
|
this.parentEl = parent;
|
||
|
this.child.moveBeforeDOMNode(node, parent);
|
||
|
parent.insertBefore(this.afterNode, node);
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
if (other) {
|
||
|
// check this with @ged-odoo for use in foreach
|
||
|
afterNode = other.firstNode() || afterNode;
|
||
|
}
|
||
|
this.child.moveBeforeVNode(other ? other.child : null, afterNode);
|
||
|
this.parentEl.insertBefore(this.afterNode, afterNode);
|
||
|
}
|
||
|
patch(other, withBeforeRemove) {
|
||
|
if (this === other) {
|
||
|
return;
|
||
|
}
|
||
|
this.handlerData = other.handlerData;
|
||
|
this.wrapHandlerData();
|
||
|
for (let i = 0; i < n; i++) {
|
||
|
this.handlerFns[i].update.call(this.parentEl, this.handlerData[i]);
|
||
|
}
|
||
|
this.child.patch(other.child, withBeforeRemove);
|
||
|
}
|
||
|
beforeRemove() {
|
||
|
this.child.beforeRemove();
|
||
|
}
|
||
|
remove() {
|
||
|
for (let i = 0; i < n; i++) {
|
||
|
this.handlerFns[i].remove.call(this.parentEl);
|
||
|
}
|
||
|
this.child.remove();
|
||
|
this.afterNode.remove();
|
||
|
}
|
||
|
firstNode() {
|
||
|
return this.child.firstNode();
|
||
|
}
|
||
|
toString() {
|
||
|
return this.child.toString();
|
||
|
}
|
||
|
}
|
||
|
return function (child, handlers) {
|
||
|
return new VCatcher(child, handlers);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function mount$1(vnode, fixture, afterNode = null) {
|
||
|
vnode.mount(fixture, afterNode);
|
||
|
}
|
||
|
function patch(vnode1, vnode2, withBeforeRemove = false) {
|
||
|
vnode1.patch(vnode2, withBeforeRemove);
|
||
|
}
|
||
|
function remove(vnode, withBeforeRemove = false) {
|
||
|
if (withBeforeRemove) {
|
||
|
vnode.beforeRemove();
|
||
|
}
|
||
|
vnode.remove();
|
||
|
}
|
||
|
|
||
|
// Maps fibers to thrown errors
|
||
|
const fibersInError = new WeakMap();
|
||
|
const nodeErrorHandlers = new WeakMap();
|
||
|
function _handleError(node, error) {
|
||
|
if (!node) {
|
||
|
return false;
|
||
|
}
|
||
|
const fiber = node.fiber;
|
||
|
if (fiber) {
|
||
|
fibersInError.set(fiber, error);
|
||
|
}
|
||
|
const errorHandlers = nodeErrorHandlers.get(node);
|
||
|
if (errorHandlers) {
|
||
|
let handled = false;
|
||
|
// execute in the opposite order
|
||
|
for (let i = errorHandlers.length - 1; i >= 0; i--) {
|
||
|
try {
|
||
|
errorHandlers[i](error);
|
||
|
handled = true;
|
||
|
break;
|
||
|
}
|
||
|
catch (e) {
|
||
|
error = e;
|
||
|
}
|
||
|
}
|
||
|
if (handled) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return _handleError(node.parent, error);
|
||
|
}
|
||
|
function handleError(params) {
|
||
|
let { error } = params;
|
||
|
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
|
||
|
if (!(error instanceof OwlError)) {
|
||
|
error = Object.assign(new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`), { cause: error });
|
||
|
}
|
||
|
const node = "node" in params ? params.node : params.fiber.node;
|
||
|
const fiber = "fiber" in params ? params.fiber : node.fiber;
|
||
|
if (fiber) {
|
||
|
// resets the fibers on components if possible. This is important so that
|
||
|
// new renderings can be properly included in the initial one, if any.
|
||
|
let current = fiber;
|
||
|
do {
|
||
|
current.node.fiber = current;
|
||
|
current = current.parent;
|
||
|
} while (current);
|
||
|
fibersInError.set(fiber.root, error);
|
||
|
}
|
||
|
const handled = _handleError(node, error);
|
||
|
if (!handled) {
|
||
|
console.warn(`[Owl] Unhandled error. Destroying the root component`);
|
||
|
try {
|
||
|
node.app.destroy();
|
||
|
}
|
||
|
catch (e) {
|
||
|
console.error(e);
|
||
|
}
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function makeChildFiber(node, parent) {
|
||
|
let current = node.fiber;
|
||
|
if (current) {
|
||
|
cancelFibers(current.children);
|
||
|
current.root = null;
|
||
|
}
|
||
|
return new Fiber(node, parent);
|
||
|
}
|
||
|
function makeRootFiber(node) {
|
||
|
let current = node.fiber;
|
||
|
if (current) {
|
||
|
let root = current.root;
|
||
|
// lock root fiber because canceling children fibers may destroy components,
|
||
|
// which means any arbitrary code can be run in onWillDestroy, which may
|
||
|
// trigger new renderings
|
||
|
root.locked = true;
|
||
|
root.setCounter(root.counter + 1 - cancelFibers(current.children));
|
||
|
root.locked = false;
|
||
|
current.children = [];
|
||
|
current.childrenMap = {};
|
||
|
current.bdom = null;
|
||
|
if (fibersInError.has(current)) {
|
||
|
fibersInError.delete(current);
|
||
|
fibersInError.delete(root);
|
||
|
current.appliedToDom = false;
|
||
|
if (current instanceof RootFiber) {
|
||
|
// it is possible that this fiber is a fiber that crashed while being
|
||
|
// mounted, so the mounted list is possibly corrupted. We restore it to
|
||
|
// its normal initial state (which is empty list or a list with a mount
|
||
|
// fiber.
|
||
|
current.mounted = current instanceof MountFiber ? [current] : [];
|
||
|
}
|
||
|
}
|
||
|
return current;
|
||
|
}
|
||
|
const fiber = new RootFiber(node, null);
|
||
|
if (node.willPatch.length) {
|
||
|
fiber.willPatch.push(fiber);
|
||
|
}
|
||
|
if (node.patched.length) {
|
||
|
fiber.patched.push(fiber);
|
||
|
}
|
||
|
return fiber;
|
||
|
}
|
||
|
function throwOnRender() {
|
||
|
throw new OwlError("Attempted to render cancelled fiber");
|
||
|
}
|
||
|
/**
|
||
|
* @returns number of not-yet rendered fibers cancelled
|
||
|
*/
|
||
|
function cancelFibers(fibers) {
|
||
|
let result = 0;
|
||
|
for (let fiber of fibers) {
|
||
|
let node = fiber.node;
|
||
|
fiber.render = throwOnRender;
|
||
|
if (node.status === 0 /* NEW */) {
|
||
|
node.cancel();
|
||
|
}
|
||
|
node.fiber = null;
|
||
|
if (fiber.bdom) {
|
||
|
// if fiber has been rendered, this means that the component props have
|
||
|
// been updated. however, this fiber will not be patched to the dom, so
|
||
|
// it could happen that the next render compare the current props with
|
||
|
// the same props, and skip the render completely. With the next line,
|
||
|
// we kindly request the component code to force a render, so it works as
|
||
|
// expected.
|
||
|
node.forceNextRender = true;
|
||
|
}
|
||
|
else {
|
||
|
result++;
|
||
|
}
|
||
|
result += cancelFibers(fiber.children);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
class Fiber {
|
||
|
constructor(node, parent) {
|
||
|
this.bdom = null;
|
||
|
this.children = [];
|
||
|
this.appliedToDom = false;
|
||
|
this.deep = false;
|
||
|
this.childrenMap = {};
|
||
|
this.node = node;
|
||
|
this.parent = parent;
|
||
|
if (parent) {
|
||
|
this.deep = parent.deep;
|
||
|
const root = parent.root;
|
||
|
root.setCounter(root.counter + 1);
|
||
|
this.root = root;
|
||
|
parent.children.push(this);
|
||
|
}
|
||
|
else {
|
||
|
this.root = this;
|
||
|
}
|
||
|
}
|
||
|
render() {
|
||
|
// if some parent has a fiber => register in followup
|
||
|
let prev = this.root.node;
|
||
|
let scheduler = prev.app.scheduler;
|
||
|
let current = prev.parent;
|
||
|
while (current) {
|
||
|
if (current.fiber) {
|
||
|
let root = current.fiber.root;
|
||
|
if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
|
||
|
current = root.node;
|
||
|
}
|
||
|
else {
|
||
|
scheduler.delayedRenders.push(this);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
prev = current;
|
||
|
current = current.parent;
|
||
|
}
|
||
|
// there are no current rendering from above => we can render
|
||
|
this._render();
|
||
|
}
|
||
|
_render() {
|
||
|
const node = this.node;
|
||
|
const root = this.root;
|
||
|
if (root) {
|
||
|
try {
|
||
|
this.bdom = true;
|
||
|
this.bdom = node.renderFn();
|
||
|
}
|
||
|
catch (e) {
|
||
|
node.app.handleError({ node, error: e });
|
||
|
}
|
||
|
root.setCounter(root.counter - 1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
class RootFiber extends Fiber {
|
||
|
constructor() {
|
||
|
super(...arguments);
|
||
|
this.counter = 1;
|
||
|
// only add stuff in this if they have registered some hooks
|
||
|
this.willPatch = [];
|
||
|
this.patched = [];
|
||
|
this.mounted = [];
|
||
|
// A fiber is typically locked when it is completing and the patch has not, or is being applied.
|
||
|
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
|
||
|
this.locked = false;
|
||
|
}
|
||
|
complete() {
|
||
|
const node = this.node;
|
||
|
this.locked = true;
|
||
|
let current = undefined;
|
||
|
let mountedFibers = this.mounted;
|
||
|
try {
|
||
|
// Step 1: calling all willPatch lifecycle hooks
|
||
|
for (current of this.willPatch) {
|
||
|
// because of the asynchronous nature of the rendering, some parts of the
|
||
|
// UI may have been rendered, then deleted in a followup rendering, and we
|
||
|
// do not want to call onWillPatch in that case.
|
||
|
let node = current.node;
|
||
|
if (node.fiber === current) {
|
||
|
const component = node.component;
|
||
|
for (let cb of node.willPatch) {
|
||
|
cb.call(component);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
current = undefined;
|
||
|
// Step 2: patching the dom
|
||
|
node._patch();
|
||
|
this.locked = false;
|
||
|
// Step 4: calling all mounted lifecycle hooks
|
||
|
while ((current = mountedFibers.pop())) {
|
||
|
current = current;
|
||
|
if (current.appliedToDom) {
|
||
|
for (let cb of current.node.mounted) {
|
||
|
cb();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// Step 5: calling all patched hooks
|
||
|
let patchedFibers = this.patched;
|
||
|
while ((current = patchedFibers.pop())) {
|
||
|
current = current;
|
||
|
if (current.appliedToDom) {
|
||
|
for (let cb of current.node.patched) {
|
||
|
cb();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (e) {
|
||
|
// if mountedFibers is not empty, this means that a crash occured while
|
||
|
// calling the mounted hooks of some component. So, there may still be
|
||
|
// some component that have been mounted, but for which the mounted hooks
|
||
|
// have not been called. Here, we remove the willUnmount hooks for these
|
||
|
// specific component to prevent a worse situation (willUnmount being
|
||
|
// called even though mounted has not been called)
|
||
|
for (let fiber of mountedFibers) {
|
||
|
fiber.node.willUnmount = [];
|
||
|
}
|
||
|
this.locked = false;
|
||
|
node.app.handleError({ fiber: current || this, error: e });
|
||
|
}
|
||
|
}
|
||
|
setCounter(newValue) {
|
||
|
this.counter = newValue;
|
||
|
if (newValue === 0) {
|
||
|
this.node.app.scheduler.flush();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
class MountFiber extends RootFiber {
|
||
|
constructor(node, target, options = {}) {
|
||
|
super(node, null);
|
||
|
this.target = target;
|
||
|
this.position = options.position || "last-child";
|
||
|
}
|
||
|
complete() {
|
||
|
let current = this;
|
||
|
try {
|
||
|
const node = this.node;
|
||
|
node.children = this.childrenMap;
|
||
|
node.app.constructor.validateTarget(this.target);
|
||
|
if (node.bdom) {
|
||
|
// this is a complicated situation: if we mount a fiber with an existing
|
||
|
// bdom, this means that this same fiber was already completed, mounted,
|
||
|
// but a crash occurred in some mounted hook. Then, it was handled and
|
||
|
// the new rendering is being applied.
|
||
|
node.updateDom();
|
||
|
}
|
||
|
else {
|
||
|
node.bdom = this.bdom;
|
||
|
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||
|
mount$1(node.bdom, this.target);
|
||
|
}
|
||
|
else {
|
||
|
const firstChild = this.target.childNodes[0];
|
||
|
mount$1(node.bdom, this.target, firstChild);
|
||
|
}
|
||
|
}
|
||
|
// unregistering the fiber before mounted since it can do another render
|
||
|
// and that the current rendering is obviously completed
|
||
|
node.fiber = null;
|
||
|
node.status = 1 /* MOUNTED */;
|
||
|
this.appliedToDom = true;
|
||
|
let mountedFibers = this.mounted;
|
||
|
while ((current = mountedFibers.pop())) {
|
||
|
if (current.appliedToDom) {
|
||
|
for (let cb of current.node.mounted) {
|
||
|
cb();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.node.app.handleError({ fiber: current, error: e });
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Special key to subscribe to, to be notified of key creation/deletion
|
||
|
const KEYCHANGES = Symbol("Key changes");
|
||
|
// Used to specify the absence of a callback, can be used as WeakMap key but
|
||
|
// should only be used as a sentinel value and never called.
|
||
|
const NO_CALLBACK = () => {
|
||
|
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
|
||
|
};
|
||
|
const objectToString = Object.prototype.toString;
|
||
|
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||
|
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||
|
const SUPPORTED_RAW_TYPES = ["Object", "Array", "Set", "Map", "WeakMap"];
|
||
|
const COLLECTION_RAW_TYPES = ["Set", "Map", "WeakMap"];
|
||
|
/**
|
||
|
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||
|
* many native objects such as Promise (whose toString is [object Promise])
|
||
|
* or Date ([object Date]), while also supporting collections without using
|
||
|
* instanceof in a loop
|
||
|
*
|
||
|
* @param obj the object to check
|
||
|
* @returns the raw type of the object
|
||
|
*/
|
||
|
function rawType(obj) {
|
||
|
return objectToString.call(toRaw(obj)).slice(8, -1);
|
||
|
}
|
||
|
/**
|
||
|
* Checks whether a given value can be made into a reactive object.
|
||
|
*
|
||
|
* @param value the value to check
|
||
|
* @returns whether the value can be made reactive
|
||
|
*/
|
||
|
function canBeMadeReactive(value) {
|
||
|
if (typeof value !== "object") {
|
||
|
return false;
|
||
|
}
|
||
|
return SUPPORTED_RAW_TYPES.includes(rawType(value));
|
||
|
}
|
||
|
/**
|
||
|
* Creates a reactive from the given object/callback if possible and returns it,
|
||
|
* returns the original object otherwise.
|
||
|
*
|
||
|
* @param value the value make reactive
|
||
|
* @returns a reactive for the given object when possible, the original otherwise
|
||
|
*/
|
||
|
function possiblyReactive(val, cb) {
|
||
|
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
||
|
}
|
||
|
const skipped = new WeakSet();
|
||
|
/**
|
||
|
* Mark an object or array so that it is ignored by the reactivity system
|
||
|
*
|
||
|
* @param value the value to mark
|
||
|
* @returns the object itself
|
||
|
*/
|
||
|
function markRaw(value) {
|
||
|
skipped.add(value);
|
||
|
return value;
|
||
|
}
|
||
|
/**
|
||
|
* Given a reactive objet, return the raw (non reactive) underlying object
|
||
|
*
|
||
|
* @param value a reactive value
|
||
|
* @returns the underlying value
|
||
|
*/
|
||
|
function toRaw(value) {
|
||
|
return targets.has(value) ? targets.get(value) : value;
|
||
|
}
|
||
|
const targetToKeysToCallbacks = new WeakMap();
|
||
|
/**
|
||
|
* Observes a given key on a target with an callback. The callback will be
|
||
|
* called when the given key changes on the target.
|
||
|
*
|
||
|
* @param target the target whose key should be observed
|
||
|
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
|
||
|
* or deletion)
|
||
|
* @param callback the function to call when the key changes
|
||
|
*/
|
||
|
function observeTargetKey(target, key, callback) {
|
||
|
if (callback === NO_CALLBACK) {
|
||
|
return;
|
||
|
}
|
||
|
if (!targetToKeysToCallbacks.get(target)) {
|
||
|
targetToKeysToCallbacks.set(target, new Map());
|
||
|
}
|
||
|
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||
|
if (!keyToCallbacks.get(key)) {
|
||
|
keyToCallbacks.set(key, new Set());
|
||
|
}
|
||
|
keyToCallbacks.get(key).add(callback);
|
||
|
if (!callbacksToTargets.has(callback)) {
|
||
|
callbacksToTargets.set(callback, new Set());
|
||
|
}
|
||
|
callbacksToTargets.get(callback).add(target);
|
||
|
}
|
||
|
/**
|
||
|
* Notify Reactives that are observing a given target that a key has changed on
|
||
|
* the target.
|
||
|
*
|
||
|
* @param target target whose Reactives should be notified that the target was
|
||
|
* changed.
|
||
|
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||
|
* or deleted)
|
||
|
*/
|
||
|
function notifyReactives(target, key) {
|
||
|
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||
|
if (!keyToCallbacks) {
|
||
|
return;
|
||
|
}
|
||
|
const callbacks = keyToCallbacks.get(key);
|
||
|
if (!callbacks) {
|
||
|
return;
|
||
|
}
|
||
|
// Loop on copy because clearReactivesForCallback will modify the set in place
|
||
|
for (const callback of [...callbacks]) {
|
||
|
clearReactivesForCallback(callback);
|
||
|
callback();
|
||
|
}
|
||
|
}
|
||
|
const callbacksToTargets = new WeakMap();
|
||
|
/**
|
||
|
* Clears all subscriptions of the Reactives associated with a given callback.
|
||
|
*
|
||
|
* @param callback the callback for which the reactives need to be cleared
|
||
|
*/
|
||
|
function clearReactivesForCallback(callback) {
|
||
|
const targetsToClear = callbacksToTargets.get(callback);
|
||
|
if (!targetsToClear) {
|
||
|
return;
|
||
|
}
|
||
|
for (const target of targetsToClear) {
|
||
|
const observedKeys = targetToKeysToCallbacks.get(target);
|
||
|
if (!observedKeys) {
|
||
|
continue;
|
||
|
}
|
||
|
for (const [key, callbacks] of observedKeys.entries()) {
|
||
|
callbacks.delete(callback);
|
||
|
if (!callbacks.size) {
|
||
|
observedKeys.delete(key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
targetsToClear.clear();
|
||
|
}
|
||
|
function getSubscriptions(callback) {
|
||
|
const targets = callbacksToTargets.get(callback) || [];
|
||
|
return [...targets].map((target) => {
|
||
|
const keysToCallbacks = targetToKeysToCallbacks.get(target);
|
||
|
let keys = [];
|
||
|
if (keysToCallbacks) {
|
||
|
for (const [key, cbs] of keysToCallbacks) {
|
||
|
if (cbs.has(callback)) {
|
||
|
keys.push(key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return { target, keys };
|
||
|
});
|
||
|
}
|
||
|
// Maps reactive objects to the underlying target
|
||
|
const targets = new WeakMap();
|
||
|
const reactiveCache = new WeakMap();
|
||
|
/**
|
||
|
* Creates a reactive proxy for an object. Reading data on the reactive object
|
||
|
* subscribes to changes to the data. Writing data on the object will cause the
|
||
|
* notify callback to be called if there are suscriptions to that data. Nested
|
||
|
* objects and arrays are automatically made reactive as well.
|
||
|
*
|
||
|
* Whenever you are notified of a change, all subscriptions are cleared, and if
|
||
|
* you would like to be notified of any further changes, you should go read
|
||
|
* the underlying data again. We assume that if you don't go read it again after
|
||
|
* being notified, it means that you are no longer interested in that data.
|
||
|
*
|
||
|
* Subscriptions:
|
||
|
* + Reading a property on an object will subscribe you to changes in the value
|
||
|
* of that property.
|
||
|
* + Accessing an object's keys (eg with Object.keys or with `for..in`) will
|
||
|
* subscribe you to the creation/deletion of keys. Checking the presence of a
|
||
|
* key on the object with 'in' has the same effect.
|
||
|
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
|
||
|
* This is a choice that was made because changing a key's value will trigger
|
||
|
* this trap and we do not want to subscribe by writes. This also means that
|
||
|
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
|
||
|
*
|
||
|
* @param target the object for which to create a reactive proxy
|
||
|
* @param callback the function to call when an observed property of the
|
||
|
* reactive has changed
|
||
|
* @returns a proxy that tracks changes to it
|
||
|
*/
|
||
|
function reactive(target, callback = NO_CALLBACK) {
|
||
|
if (!canBeMadeReactive(target)) {
|
||
|
throw new OwlError(`Cannot make the given value reactive`);
|
||
|
}
|
||
|
if (skipped.has(target)) {
|
||
|
return target;
|
||
|
}
|
||
|
if (targets.has(target)) {
|
||
|
// target is reactive, create a reactive on the underlying object instead
|
||
|
return reactive(targets.get(target), callback);
|
||
|
}
|
||
|
if (!reactiveCache.has(target)) {
|
||
|
reactiveCache.set(target, new WeakMap());
|
||
|
}
|
||
|
const reactivesForTarget = reactiveCache.get(target);
|
||
|
if (!reactivesForTarget.has(callback)) {
|
||
|
const targetRawType = rawType(target);
|
||
|
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||
|
? collectionsProxyHandler(target, callback, targetRawType)
|
||
|
: basicProxyHandler(callback);
|
||
|
const proxy = new Proxy(target, handler);
|
||
|
reactivesForTarget.set(callback, proxy);
|
||
|
targets.set(proxy, target);
|
||
|
}
|
||
|
return reactivesForTarget.get(callback);
|
||
|
}
|
||
|
/**
|
||
|
* Creates a basic proxy handler for regular objects and arrays.
|
||
|
*
|
||
|
* @param callback @see reactive
|
||
|
* @returns a proxy handler object
|
||
|
*/
|
||
|
function basicProxyHandler(callback) {
|
||
|
return {
|
||
|
get(target, key, receiver) {
|
||
|
// non-writable non-configurable properties cannot be made reactive
|
||
|
const desc = Object.getOwnPropertyDescriptor(target, key);
|
||
|
if (desc && !desc.writable && !desc.configurable) {
|
||
|
return Reflect.get(target, key, receiver);
|
||
|
}
|
||
|
observeTargetKey(target, key, callback);
|
||
|
return possiblyReactive(Reflect.get(target, key, receiver), callback);
|
||
|
},
|
||
|
set(target, key, value, receiver) {
|
||
|
const hadKey = objectHasOwnProperty.call(target, key);
|
||
|
const originalValue = Reflect.get(target, key, receiver);
|
||
|
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
||
|
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
||
|
notifyReactives(target, KEYCHANGES);
|
||
|
}
|
||
|
// While Array length may trigger the set trap, it's not actually set by this
|
||
|
// method but is updated behind the scenes, and the trap is not called with the
|
||
|
// new value. We disable the "same-value-optimization" for it because of that.
|
||
|
if (originalValue !== Reflect.get(target, key, receiver) ||
|
||
|
(key === "length" && Array.isArray(target))) {
|
||
|
notifyReactives(target, key);
|
||
|
}
|
||
|
return ret;
|
||
|
},
|
||
|
deleteProperty(target, key) {
|
||
|
const ret = Reflect.deleteProperty(target, key);
|
||
|
// TODO: only notify when something was actually deleted
|
||
|
notifyReactives(target, KEYCHANGES);
|
||
|
notifyReactives(target, key);
|
||
|
return ret;
|
||
|
},
|
||
|
ownKeys(target) {
|
||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||
|
return Reflect.ownKeys(target);
|
||
|
},
|
||
|
has(target, key) {
|
||
|
// TODO: this observes all key changes instead of only the presence of the argument key
|
||
|
// observing the key itself would observe value changes instead of presence changes
|
||
|
// so we may need a finer grained system to distinguish observing value vs presence.
|
||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||
|
return Reflect.has(target, key);
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Creates a function that will observe the key that is passed to it when called
|
||
|
* and delegates to the underlying method.
|
||
|
*
|
||
|
* @param methodName name of the method to delegate to
|
||
|
* @param target @see reactive
|
||
|
* @param callback @see reactive
|
||
|
*/
|
||
|
function makeKeyObserver(methodName, target, callback) {
|
||
|
return (key) => {
|
||
|
key = toRaw(key);
|
||
|
observeTargetKey(target, key, callback);
|
||
|
return possiblyReactive(target[methodName](key), callback);
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Creates an iterable that will delegate to the underlying iteration method and
|
||
|
* observe keys as necessary.
|
||
|
*
|
||
|
* @param methodName name of the method to delegate to
|
||
|
* @param target @see reactive
|
||
|
* @param callback @see reactive
|
||
|
*/
|
||
|
function makeIteratorObserver(methodName, target, callback) {
|
||
|
return function* () {
|
||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||
|
const keys = target.keys();
|
||
|
for (const item of target[methodName]()) {
|
||
|
const key = keys.next().value;
|
||
|
observeTargetKey(target, key, callback);
|
||
|
yield possiblyReactive(item, callback);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Creates a forEach function that will delegate to forEach on the underlying
|
||
|
* collection while observing key changes, and keys as they're iterated over,
|
||
|
* and making the passed keys/values reactive.
|
||
|
*
|
||
|
* @param target @see reactive
|
||
|
* @param callback @see reactive
|
||
|
*/
|
||
|
function makeForEachObserver(target, callback) {
|
||
|
return function forEach(forEachCb, thisArg) {
|
||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||
|
target.forEach(function (val, key, targetObj) {
|
||
|
observeTargetKey(target, key, callback);
|
||
|
forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));
|
||
|
}, thisArg);
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Creates a function that will delegate to an underlying method, and check if
|
||
|
* that method has modified the presence or value of a key, and notify the
|
||
|
* reactives appropriately.
|
||
|
*
|
||
|
* @param setterName name of the method to delegate to
|
||
|
* @param getterName name of the method which should be used to retrieve the
|
||
|
* value before calling the delegate method for comparison purposes
|
||
|
* @param target @see reactive
|
||
|
*/
|
||
|
function delegateAndNotify(setterName, getterName, target) {
|
||
|
return (key, value) => {
|
||
|
key = toRaw(key);
|
||
|
const hadKey = target.has(key);
|
||
|
const originalValue = target[getterName](key);
|
||
|
const ret = target[setterName](key, value);
|
||
|
const hasKey = target.has(key);
|
||
|
if (hadKey !== hasKey) {
|
||
|
notifyReactives(target, KEYCHANGES);
|
||
|
}
|
||
|
if (originalValue !== target[getterName](key)) {
|
||
|
notifyReactives(target, key);
|
||
|
}
|
||
|
return ret;
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Creates a function that will clear the underlying collection and notify that
|
||
|
* the keys of the collection have changed.
|
||
|
*
|
||
|
* @param target @see reactive
|
||
|
*/
|
||
|
function makeClearNotifier(target) {
|
||
|
return () => {
|
||
|
const allKeys = [...target.keys()];
|
||
|
target.clear();
|
||
|
notifyReactives(target, KEYCHANGES);
|
||
|
for (const key of allKeys) {
|
||
|
notifyReactives(target, key);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* Maps raw type of an object to an object containing functions that can be used
|
||
|
* to build an appropritate proxy handler for that raw type. Eg: when making a
|
||
|
* reactive set, calling the has method should mark the key that is being
|
||
|
* retrieved as observed, and calling the add or delete method should notify the
|
||
|
* reactives that the key which is being added or deleted has been modified.
|
||
|
*/
|
||
|
const rawTypeToFuncHandlers = {
|
||
|
Set: (target, callback) => ({
|
||
|
has: makeKeyObserver("has", target, callback),
|
||
|
add: delegateAndNotify("add", "has", target),
|
||
|
delete: delegateAndNotify("delete", "has", target),
|
||
|
keys: makeIteratorObserver("keys", target, callback),
|
||
|
values: makeIteratorObserver("values", target, callback),
|
||
|
entries: makeIteratorObserver("entries", target, callback),
|
||
|
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||
|
forEach: makeForEachObserver(target, callback),
|
||
|
clear: makeClearNotifier(target),
|
||
|
get size() {
|
||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||
|
return target.size;
|
||
|
},
|
||
|
}),
|
||
|
Map: (target, callback) => ({
|
||
|
has: makeKeyObserver("has", target, callback),
|
||
|
get: makeKeyObserver("get", target, callback),
|
||
|
set: delegateAndNotify("set", "get", target),
|
||
|
delete: delegateAndNotify("delete", "has", target),
|
||
|
keys: makeIteratorObserver("keys", target, callback),
|
||
|
values: makeIteratorObserver("values", target, callback),
|
||
|
entries: makeIteratorObserver("entries", target, callback),
|
||
|
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||
|
forEach: makeForEachObserver(target, callback),
|
||
|
clear: makeClearNotifier(target),
|
||
|
get size() {
|
||
|
observeTargetKey(target, KEYCHANGES, callback);
|
||
|
return target.size;
|
||
|
},
|
||
|
}),
|
||
|
WeakMap: (target, callback) => ({
|
||
|
has: makeKeyObserver("has", target, callback),
|
||
|
get: makeKeyObserver("get", target, callback),
|
||
|
set: delegateAndNotify("set", "get", target),
|
||
|
delete: delegateAndNotify("delete", "has", target),
|
||
|
}),
|
||
|
};
|
||
|
/**
|
||
|
* Creates a proxy handler for collections (Set/Map/WeakMap)
|
||
|
*
|
||
|
* @param callback @see reactive
|
||
|
* @param target @see reactive
|
||
|
* @returns a proxy handler object
|
||
|
*/
|
||
|
function collectionsProxyHandler(target, callback, targetRawType) {
|
||
|
// TODO: if performance is an issue we can create the special handlers lazily when each
|
||
|
// property is read.
|
||
|
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
|
||
|
return Object.assign(basicProxyHandler(callback), {
|
||
|
// FIXME: probably broken when part of prototype chain since we ignore the receiver
|
||
|
get(target, key) {
|
||
|
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
||
|
return specialHandlers[key];
|
||
|
}
|
||
|
observeTargetKey(target, key, callback);
|
||
|
return possiblyReactive(target[key], callback);
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
let currentNode = null;
|
||
|
function saveCurrent() {
|
||
|
let n = currentNode;
|
||
|
return () => {
|
||
|
currentNode = n;
|
||
|
};
|
||
|
}
|
||
|
function getCurrent() {
|
||
|
if (!currentNode) {
|
||
|
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
||
|
}
|
||
|
return currentNode;
|
||
|
}
|
||
|
function useComponent() {
|
||
|
return currentNode.component;
|
||
|
}
|
||
|
/**
|
||
|
* Apply default props (only top level).
|
||
|
*/
|
||
|
function applyDefaultProps(props, defaultProps) {
|
||
|
for (let propName in defaultProps) {
|
||
|
if (props[propName] === undefined) {
|
||
|
props[propName] = defaultProps[propName];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Integration with reactivity system (useState)
|
||
|
// -----------------------------------------------------------------------------
|
||
|
const batchedRenderFunctions = new WeakMap();
|
||
|
/**
|
||
|
* Creates a reactive object that will be observed by the current component.
|
||
|
* Reading data from the returned object (eg during rendering) will cause the
|
||
|
* component to subscribe to that data and be rerendered when it changes.
|
||
|
*
|
||
|
* @param state the state to observe
|
||
|
* @returns a reactive object that will cause the component to re-render on
|
||
|
* relevant changes
|
||
|
* @see reactive
|
||
|
*/
|
||
|
function useState(state) {
|
||
|
const node = getCurrent();
|
||
|
let render = batchedRenderFunctions.get(node);
|
||
|
if (!render) {
|
||
|
render = batched(node.render.bind(node, false));
|
||
|
batchedRenderFunctions.set(node, render);
|
||
|
// manual implementation of onWillDestroy to break cyclic dependency
|
||
|
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||
|
}
|
||
|
return reactive(state, render);
|
||
|
}
|
||
|
class ComponentNode {
|
||
|
constructor(C, props, app, parent, parentKey) {
|
||
|
this.fiber = null;
|
||
|
this.bdom = null;
|
||
|
this.status = 0 /* NEW */;
|
||
|
this.forceNextRender = false;
|
||
|
this.nextProps = null;
|
||
|
this.children = Object.create(null);
|
||
|
this.refs = {};
|
||
|
this.willStart = [];
|
||
|
this.willUpdateProps = [];
|
||
|
this.willUnmount = [];
|
||
|
this.mounted = [];
|
||
|
this.willPatch = [];
|
||
|
this.patched = [];
|
||
|
this.willDestroy = [];
|
||
|
currentNode = this;
|
||
|
this.app = app;
|
||
|
this.parent = parent;
|
||
|
this.props = props;
|
||
|
this.parentKey = parentKey;
|
||
|
const defaultProps = C.defaultProps;
|
||
|
props = Object.assign({}, props);
|
||
|
if (defaultProps) {
|
||
|
applyDefaultProps(props, defaultProps);
|
||
|
}
|
||
|
const env = (parent && parent.childEnv) || app.env;
|
||
|
this.childEnv = env;
|
||
|
for (const key in props) {
|
||
|
const prop = props[key];
|
||
|
if (prop && typeof prop === "object" && targets.has(prop)) {
|
||
|
props[key] = useState(prop);
|
||
|
}
|
||
|
}
|
||
|
this.component = new C(props, env, this);
|
||
|
const ctx = Object.assign(Object.create(this.component), { this: this.component });
|
||
|
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
|
||
|
this.component.setup();
|
||
|
currentNode = null;
|
||
|
}
|
||
|
mountComponent(target, options) {
|
||
|
const fiber = new MountFiber(this, target, options);
|
||
|
this.app.scheduler.addFiber(fiber);
|
||
|
this.initiateRender(fiber);
|
||
|
}
|
||
|
async initiateRender(fiber) {
|
||
|
this.fiber = fiber;
|
||
|
if (this.mounted.length) {
|
||
|
fiber.root.mounted.push(fiber);
|
||
|
}
|
||
|
const component = this.component;
|
||
|
try {
|
||
|
await Promise.all(this.willStart.map((f) => f.call(component)));
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.app.handleError({ node: this, error: e });
|
||
|
return;
|
||
|
}
|
||
|
if (this.status === 0 /* NEW */ && this.fiber === fiber) {
|
||
|
fiber.render();
|
||
|
}
|
||
|
}
|
||
|
async render(deep) {
|
||
|
if (this.status >= 2 /* CANCELLED */) {
|
||
|
return;
|
||
|
}
|
||
|
let current = this.fiber;
|
||
|
if (current && (current.root.locked || current.bdom === true)) {
|
||
|
await Promise.resolve();
|
||
|
// situation may have changed after the microtask tick
|
||
|
current = this.fiber;
|
||
|
}
|
||
|
if (current) {
|
||
|
if (!current.bdom && !fibersInError.has(current)) {
|
||
|
if (deep) {
|
||
|
// we want the render from this point on to be with deep=true
|
||
|
current.deep = deep;
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
// if current rendering was with deep=true, we want this one to be the same
|
||
|
deep = deep || current.deep;
|
||
|
}
|
||
|
else if (!this.bdom) {
|
||
|
return;
|
||
|
}
|
||
|
const fiber = makeRootFiber(this);
|
||
|
fiber.deep = deep;
|
||
|
this.fiber = fiber;
|
||
|
this.app.scheduler.addFiber(fiber);
|
||
|
await Promise.resolve();
|
||
|
if (this.status >= 2 /* CANCELLED */) {
|
||
|
return;
|
||
|
}
|
||
|
// We only want to actually render the component if the following two
|
||
|
// conditions are true:
|
||
|
// * this.fiber: it could be null, in which case the render has been cancelled
|
||
|
// * (current || !fiber.parent): if current is not null, this means that the
|
||
|
// render function was called when a render was already occurring. In this
|
||
|
// case, the pending rendering was cancelled, and the fiber needs to be
|
||
|
// rendered to complete the work. If current is null, we check that the
|
||
|
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||
|
// a root fiber to a child fiber in the previous microtick, because it was
|
||
|
// embedded in a rendering coming from above, so the fiber will be rendered
|
||
|
// in the next microtick anyway, so we should not render it again.
|
||
|
if (this.fiber === fiber && (current || !fiber.parent)) {
|
||
|
fiber.render();
|
||
|
}
|
||
|
}
|
||
|
cancel() {
|
||
|
this._cancel();
|
||
|
delete this.parent.children[this.parentKey];
|
||
|
this.app.scheduler.scheduleDestroy(this);
|
||
|
}
|
||
|
_cancel() {
|
||
|
this.status = 2 /* CANCELLED */;
|
||
|
const children = this.children;
|
||
|
for (let childKey in children) {
|
||
|
children[childKey]._cancel();
|
||
|
}
|
||
|
}
|
||
|
destroy() {
|
||
|
let shouldRemove = this.status === 1 /* MOUNTED */;
|
||
|
this._destroy();
|
||
|
if (shouldRemove) {
|
||
|
this.bdom.remove();
|
||
|
}
|
||
|
}
|
||
|
_destroy() {
|
||
|
const component = this.component;
|
||
|
if (this.status === 1 /* MOUNTED */) {
|
||
|
for (let cb of this.willUnmount) {
|
||
|
cb.call(component);
|
||
|
}
|
||
|
}
|
||
|
for (let child of Object.values(this.children)) {
|
||
|
child._destroy();
|
||
|
}
|
||
|
if (this.willDestroy.length) {
|
||
|
try {
|
||
|
for (let cb of this.willDestroy) {
|
||
|
cb.call(component);
|
||
|
}
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.app.handleError({ error: e, node: this });
|
||
|
}
|
||
|
}
|
||
|
this.status = 3 /* DESTROYED */;
|
||
|
}
|
||
|
async updateAndRender(props, parentFiber) {
|
||
|
this.nextProps = props;
|
||
|
props = Object.assign({}, props);
|
||
|
// update
|
||
|
const fiber = makeChildFiber(this, parentFiber);
|
||
|
this.fiber = fiber;
|
||
|
const component = this.component;
|
||
|
const defaultProps = component.constructor.defaultProps;
|
||
|
if (defaultProps) {
|
||
|
applyDefaultProps(props, defaultProps);
|
||
|
}
|
||
|
currentNode = this;
|
||
|
for (const key in props) {
|
||
|
const prop = props[key];
|
||
|
if (prop && typeof prop === "object" && targets.has(prop)) {
|
||
|
props[key] = useState(prop);
|
||
|
}
|
||
|
}
|
||
|
currentNode = null;
|
||
|
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
||
|
await prom;
|
||
|
if (fiber !== this.fiber) {
|
||
|
return;
|
||
|
}
|
||
|
component.props = props;
|
||
|
fiber.render();
|
||
|
const parentRoot = parentFiber.root;
|
||
|
if (this.willPatch.length) {
|
||
|
parentRoot.willPatch.push(fiber);
|
||
|
}
|
||
|
if (this.patched.length) {
|
||
|
parentRoot.patched.push(fiber);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Finds a child that has dom that is not yet updated, and update it. This
|
||
|
* method is meant to be used only in the context of repatching the dom after
|
||
|
* a mounted hook failed and was handled.
|
||
|
*/
|
||
|
updateDom() {
|
||
|
if (!this.fiber) {
|
||
|
return;
|
||
|
}
|
||
|
if (this.bdom === this.fiber.bdom) {
|
||
|
// If the error was handled by some child component, we need to find it to
|
||
|
// apply its change
|
||
|
for (let k in this.children) {
|
||
|
const child = this.children[k];
|
||
|
child.updateDom();
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
// if we get here, this is the component that handled the error and rerendered
|
||
|
// itself, so we can simply patch the dom
|
||
|
this.bdom.patch(this.fiber.bdom, false);
|
||
|
this.fiber.appliedToDom = true;
|
||
|
this.fiber = null;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Sets a ref to a given HTMLElement.
|
||
|
*
|
||
|
* @param name the name of the ref to set
|
||
|
* @param el the HTMLElement to set the ref to. The ref is not set if the el
|
||
|
* is null, but useRef will not return elements that are not in the DOM
|
||
|
*/
|
||
|
setRef(name, el) {
|
||
|
if (el) {
|
||
|
this.refs[name] = el;
|
||
|
}
|
||
|
}
|
||
|
// ---------------------------------------------------------------------------
|
||
|
// Block DOM methods
|
||
|
// ---------------------------------------------------------------------------
|
||
|
firstNode() {
|
||
|
const bdom = this.bdom;
|
||
|
return bdom ? bdom.firstNode() : undefined;
|
||
|
}
|
||
|
mount(parent, anchor) {
|
||
|
const bdom = this.fiber.bdom;
|
||
|
this.bdom = bdom;
|
||
|
bdom.mount(parent, anchor);
|
||
|
this.status = 1 /* MOUNTED */;
|
||
|
this.fiber.appliedToDom = true;
|
||
|
this.children = this.fiber.childrenMap;
|
||
|
this.fiber = null;
|
||
|
}
|
||
|
moveBeforeDOMNode(node, parent) {
|
||
|
this.bdom.moveBeforeDOMNode(node, parent);
|
||
|
}
|
||
|
moveBeforeVNode(other, afterNode) {
|
||
|
this.bdom.moveBeforeVNode(other ? other.bdom : null, afterNode);
|
||
|
}
|
||
|
patch() {
|
||
|
if (this.fiber && this.fiber.parent) {
|
||
|
// we only patch here renderings coming from above. renderings initiated
|
||
|
// by the component will be patched independently in the appropriate
|
||
|
// fiber.complete
|
||
|
this._patch();
|
||
|
this.props = this.nextProps;
|
||
|
}
|
||
|
}
|
||
|
_patch() {
|
||
|
let hasChildren = false;
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
for (let _k in this.children) {
|
||
|
hasChildren = true;
|
||
|
break;
|
||
|
}
|
||
|
const fiber = this.fiber;
|
||
|
this.children = fiber.childrenMap;
|
||
|
this.bdom.patch(fiber.bdom, hasChildren);
|
||
|
fiber.appliedToDom = true;
|
||
|
this.fiber = null;
|
||
|
}
|
||
|
beforeRemove() {
|
||
|
this._destroy();
|
||
|
}
|
||
|
remove() {
|
||
|
this.bdom.remove();
|
||
|
}
|
||
|
// ---------------------------------------------------------------------------
|
||
|
// Some debug helpers
|
||
|
// ---------------------------------------------------------------------------
|
||
|
get name() {
|
||
|
return this.component.constructor.name;
|
||
|
}
|
||
|
get subscriptions() {
|
||
|
const render = batchedRenderFunctions.get(this);
|
||
|
return render ? getSubscriptions(render) : [];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const TIMEOUT = Symbol("timeout");
|
||
|
const HOOK_TIMEOUT = {
|
||
|
onWillStart: 3000,
|
||
|
onWillUpdateProps: 3000,
|
||
|
};
|
||
|
function wrapError(fn, hookName) {
|
||
|
const error = new OwlError();
|
||
|
const timeoutError = new OwlError();
|
||
|
const node = getCurrent();
|
||
|
return (...args) => {
|
||
|
const onError = (cause) => {
|
||
|
error.cause = cause;
|
||
|
error.message =
|
||
|
cause instanceof Error
|
||
|
? `The following error occurred in ${hookName}: "${cause.message}"`
|
||
|
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||
|
throw error;
|
||
|
};
|
||
|
let result;
|
||
|
try {
|
||
|
result = fn(...args);
|
||
|
}
|
||
|
catch (cause) {
|
||
|
onError(cause);
|
||
|
}
|
||
|
if (!(result instanceof Promise)) {
|
||
|
return result;
|
||
|
}
|
||
|
const timeout = HOOK_TIMEOUT[hookName];
|
||
|
if (timeout) {
|
||
|
const fiber = node.fiber;
|
||
|
Promise.race([
|
||
|
result.catch(() => { }),
|
||
|
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
|
||
|
]).then((res) => {
|
||
|
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||
|
timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;
|
||
|
console.log(timeoutError);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
return result.catch(onError);
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// hooks
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function onWillStart(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
|
||
|
}
|
||
|
function onWillUpdateProps(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
|
||
|
}
|
||
|
function onMounted(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
|
||
|
}
|
||
|
function onWillPatch(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
|
||
|
}
|
||
|
function onPatched(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
|
||
|
}
|
||
|
function onWillUnmount(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
|
||
|
}
|
||
|
function onWillDestroy(fn) {
|
||
|
const node = getCurrent();
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
|
||
|
}
|
||
|
function onWillRender(fn) {
|
||
|
const node = getCurrent();
|
||
|
const renderFn = node.renderFn;
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
fn = decorate(fn.bind(node.component), "onWillRender");
|
||
|
node.renderFn = () => {
|
||
|
fn();
|
||
|
return renderFn();
|
||
|
};
|
||
|
}
|
||
|
function onRendered(fn) {
|
||
|
const node = getCurrent();
|
||
|
const renderFn = node.renderFn;
|
||
|
const decorate = node.app.dev ? wrapError : (fn) => fn;
|
||
|
fn = decorate(fn.bind(node.component), "onRendered");
|
||
|
node.renderFn = () => {
|
||
|
const result = renderFn();
|
||
|
fn();
|
||
|
return result;
|
||
|
};
|
||
|
}
|
||
|
function onError(callback) {
|
||
|
const node = getCurrent();
|
||
|
let handlers = nodeErrorHandlers.get(node);
|
||
|
if (!handlers) {
|
||
|
handlers = [];
|
||
|
nodeErrorHandlers.set(node, handlers);
|
||
|
}
|
||
|
handlers.push(callback.bind(node.component));
|
||
|
}
|
||
|
|
||
|
class Component {
|
||
|
constructor(props, env, node) {
|
||
|
this.props = props;
|
||
|
this.env = env;
|
||
|
this.__owl__ = node;
|
||
|
}
|
||
|
setup() { }
|
||
|
render(deep = false) {
|
||
|
this.__owl__.render(deep === true);
|
||
|
}
|
||
|
}
|
||
|
Component.template = "";
|
||
|
|
||
|
const VText = text("").constructor;
|
||
|
class VPortal extends VText {
|
||
|
constructor(selector, content) {
|
||
|
super("");
|
||
|
this.target = null;
|
||
|
this.selector = selector;
|
||
|
this.content = content;
|
||
|
}
|
||
|
mount(parent, anchor) {
|
||
|
super.mount(parent, anchor);
|
||
|
this.target = document.querySelector(this.selector);
|
||
|
if (this.target) {
|
||
|
this.content.mount(this.target, null);
|
||
|
}
|
||
|
else {
|
||
|
this.content.mount(parent, anchor);
|
||
|
}
|
||
|
}
|
||
|
beforeRemove() {
|
||
|
this.content.beforeRemove();
|
||
|
}
|
||
|
remove() {
|
||
|
if (this.content) {
|
||
|
super.remove();
|
||
|
this.content.remove();
|
||
|
this.content = null;
|
||
|
}
|
||
|
}
|
||
|
patch(other) {
|
||
|
super.patch(other);
|
||
|
if (this.content) {
|
||
|
this.content.patch(other.content, true);
|
||
|
}
|
||
|
else {
|
||
|
this.content = other.content;
|
||
|
this.content.mount(this.target, null);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* kind of similar to <t t-slot="default"/>, but it wraps it around a VPortal
|
||
|
*/
|
||
|
function portalTemplate(app, bdom, helpers) {
|
||
|
let { callSlot } = helpers;
|
||
|
return function template(ctx, node, key = "") {
|
||
|
return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null));
|
||
|
};
|
||
|
}
|
||
|
class Portal extends Component {
|
||
|
setup() {
|
||
|
const node = this.__owl__;
|
||
|
onMounted(() => {
|
||
|
const portal = node.bdom;
|
||
|
if (!portal.target) {
|
||
|
const target = document.querySelector(this.props.target);
|
||
|
if (target) {
|
||
|
portal.content.moveBeforeDOMNode(target.firstChild, target);
|
||
|
}
|
||
|
else {
|
||
|
throw new OwlError("invalid portal target");
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
onWillUnmount(() => {
|
||
|
const portal = node.bdom;
|
||
|
portal.remove();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
Portal.template = "__portal__";
|
||
|
Portal.props = {
|
||
|
target: {
|
||
|
type: String,
|
||
|
},
|
||
|
slots: true,
|
||
|
};
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// helpers
|
||
|
// -----------------------------------------------------------------------------
|
||
|
const isUnionType = (t) => Array.isArray(t);
|
||
|
const isBaseType = (t) => typeof t !== "object";
|
||
|
const isValueType = (t) => typeof t === "object" && t && "value" in t;
|
||
|
function isOptional(t) {
|
||
|
return typeof t === "object" && "optional" in t ? t.optional || false : false;
|
||
|
}
|
||
|
function describeType(type) {
|
||
|
return type === "*" || type === true ? "value" : type.name.toLowerCase();
|
||
|
}
|
||
|
function describe(info) {
|
||
|
if (isBaseType(info)) {
|
||
|
return describeType(info);
|
||
|
}
|
||
|
else if (isUnionType(info)) {
|
||
|
return info.map(describe).join(" or ");
|
||
|
}
|
||
|
else if (isValueType(info)) {
|
||
|
return String(info.value);
|
||
|
}
|
||
|
if ("element" in info) {
|
||
|
return `list of ${describe({ type: info.element, optional: false })}s`;
|
||
|
}
|
||
|
if ("shape" in info) {
|
||
|
return `object`;
|
||
|
}
|
||
|
return describe(info.type || "*");
|
||
|
}
|
||
|
function toSchema(spec) {
|
||
|
return Object.fromEntries(spec.map((e) => e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]));
|
||
|
}
|
||
|
/**
|
||
|
* Main validate function
|
||
|
*/
|
||
|
function validate(obj, spec) {
|
||
|
let errors = validateSchema(obj, spec);
|
||
|
if (errors.length) {
|
||
|
throw new OwlError("Invalid object: " + errors.join(", "));
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Helper validate function, to get the list of errors. useful if one want to
|
||
|
* manipulate the errors without parsing an error object
|
||
|
*/
|
||
|
function validateSchema(obj, schema) {
|
||
|
if (Array.isArray(schema)) {
|
||
|
schema = toSchema(schema);
|
||
|
}
|
||
|
obj = toRaw(obj);
|
||
|
let errors = [];
|
||
|
// check if each value in obj has correct shape
|
||
|
for (let key in obj) {
|
||
|
if (key in schema) {
|
||
|
let result = validateType(key, obj[key], schema[key]);
|
||
|
if (result) {
|
||
|
errors.push(result);
|
||
|
}
|
||
|
}
|
||
|
else if (!("*" in schema)) {
|
||
|
errors.push(`unknown key '${key}'`);
|
||
|
}
|
||
|
}
|
||
|
// check that all specified keys are defined in obj
|
||
|
for (let key in schema) {
|
||
|
const spec = schema[key];
|
||
|
if (key !== "*" && !isOptional(spec) && !(key in obj)) {
|
||
|
const isObj = typeof spec === "object" && !Array.isArray(spec);
|
||
|
const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
|
||
|
let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
|
||
|
errors.push(`'${key}' is missing${detail}`);
|
||
|
}
|
||
|
}
|
||
|
return errors;
|
||
|
}
|
||
|
function validateBaseType(key, value, type) {
|
||
|
if (typeof type === "function") {
|
||
|
if (typeof value === "object") {
|
||
|
if (!(value instanceof type)) {
|
||
|
return `'${key}' is not a ${describeType(type)}`;
|
||
|
}
|
||
|
}
|
||
|
else if (typeof value !== type.name.toLowerCase()) {
|
||
|
return `'${key}' is not a ${describeType(type)}`;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
function validateArrayType(key, value, descr) {
|
||
|
if (!Array.isArray(value)) {
|
||
|
return `'${key}' is not a list of ${describe(descr)}s`;
|
||
|
}
|
||
|
for (let i = 0; i < value.length; i++) {
|
||
|
const error = validateType(`${key}[${i}]`, value[i], descr);
|
||
|
if (error) {
|
||
|
return error;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
function validateType(key, value, descr) {
|
||
|
if (value === undefined) {
|
||
|
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
|
||
|
}
|
||
|
else if (isBaseType(descr)) {
|
||
|
return validateBaseType(key, value, descr);
|
||
|
}
|
||
|
else if (isValueType(descr)) {
|
||
|
return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
|
||
|
}
|
||
|
else if (isUnionType(descr)) {
|
||
|
let validDescr = descr.find((p) => !validateType(key, value, p));
|
||
|
return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
|
||
|
}
|
||
|
let result = null;
|
||
|
if ("element" in descr) {
|
||
|
result = validateArrayType(key, value, descr.element);
|
||
|
}
|
||
|
else if ("shape" in descr) {
|
||
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||
|
result = `'${key}' is not an object`;
|
||
|
}
|
||
|
else {
|
||
|
const errors = validateSchema(value, descr.shape);
|
||
|
if (errors.length) {
|
||
|
result = `'${key}' doesn't have the correct shape (${errors.join(", ")})`;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if ("values" in descr) {
|
||
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||
|
result = `'${key}' is not an object`;
|
||
|
}
|
||
|
else {
|
||
|
const errors = Object.entries(value)
|
||
|
.map(([key, value]) => validateType(key, value, descr.values))
|
||
|
.filter(Boolean);
|
||
|
if (errors.length) {
|
||
|
result = `some of the values in '${key}' are invalid (${errors.join(", ")})`;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if ("type" in descr && !result) {
|
||
|
result = validateType(key, value, descr.type);
|
||
|
}
|
||
|
if ("validate" in descr && !result) {
|
||
|
result = !descr.validate(value) ? `'${key}' is not valid` : null;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
const ObjectCreate = Object.create;
|
||
|
/**
|
||
|
* This file contains utility functions that will be injected in each template,
|
||
|
* to perform various useful tasks in the compiled code.
|
||
|
*/
|
||
|
function withDefault(value, defaultValue) {
|
||
|
return value === undefined || value === null || value === false ? defaultValue : value;
|
||
|
}
|
||
|
function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
|
||
|
key = key + "__slot_" + name;
|
||
|
const slots = ctx.props.slots || {};
|
||
|
const { __render, __ctx, __scope } = slots[name] || {};
|
||
|
const slotScope = ObjectCreate(__ctx || {});
|
||
|
if (__scope) {
|
||
|
slotScope[__scope] = extra;
|
||
|
}
|
||
|
const slotBDom = __render ? __render(slotScope, parent, key) : null;
|
||
|
if (defaultContent) {
|
||
|
let child1 = undefined;
|
||
|
let child2 = undefined;
|
||
|
if (slotBDom) {
|
||
|
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||
|
}
|
||
|
else {
|
||
|
child2 = defaultContent(ctx, parent, key);
|
||
|
}
|
||
|
return multi([child1, child2]);
|
||
|
}
|
||
|
return slotBDom || text("");
|
||
|
}
|
||
|
function capture(ctx) {
|
||
|
const result = ObjectCreate(ctx);
|
||
|
for (let k in ctx) {
|
||
|
result[k] = ctx[k];
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
function withKey(elem, k) {
|
||
|
elem.key = k;
|
||
|
return elem;
|
||
|
}
|
||
|
function prepareList(collection) {
|
||
|
let keys;
|
||
|
let values;
|
||
|
if (Array.isArray(collection)) {
|
||
|
keys = collection;
|
||
|
values = collection;
|
||
|
}
|
||
|
else if (collection instanceof Map) {
|
||
|
keys = [...collection.keys()];
|
||
|
values = [...collection.values()];
|
||
|
}
|
||
|
else if (Symbol.iterator in Object(collection)) {
|
||
|
keys = [...collection];
|
||
|
values = keys;
|
||
|
}
|
||
|
else if (collection && typeof collection === "object") {
|
||
|
values = Object.values(collection);
|
||
|
keys = Object.keys(collection);
|
||
|
}
|
||
|
else {
|
||
|
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
|
||
|
}
|
||
|
const n = values.length;
|
||
|
return [keys, values, n, new Array(n)];
|
||
|
}
|
||
|
const isBoundary = Symbol("isBoundary");
|
||
|
function setContextValue(ctx, key, value) {
|
||
|
const ctx0 = ctx;
|
||
|
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
|
||
|
const newCtx = ctx.__proto__;
|
||
|
if (!newCtx) {
|
||
|
ctx = ctx0;
|
||
|
break;
|
||
|
}
|
||
|
ctx = newCtx;
|
||
|
}
|
||
|
ctx[key] = value;
|
||
|
}
|
||
|
function toNumber(val) {
|
||
|
const n = parseFloat(val);
|
||
|
return isNaN(n) ? val : n;
|
||
|
}
|
||
|
function shallowEqual(l1, l2) {
|
||
|
for (let i = 0, l = l1.length; i < l; i++) {
|
||
|
if (l1[i] !== l2[i]) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
class LazyValue {
|
||
|
constructor(fn, ctx, component, node, key) {
|
||
|
this.fn = fn;
|
||
|
this.ctx = capture(ctx);
|
||
|
this.component = component;
|
||
|
this.node = node;
|
||
|
this.key = key;
|
||
|
}
|
||
|
evaluate() {
|
||
|
return this.fn.call(this.component, this.ctx, this.node, this.key);
|
||
|
}
|
||
|
toString() {
|
||
|
return this.evaluate().toString();
|
||
|
}
|
||
|
}
|
||
|
/*
|
||
|
* Safely outputs `value` as a block depending on the nature of `value`
|
||
|
*/
|
||
|
function safeOutput(value, defaultValue) {
|
||
|
if (value === undefined || value === null) {
|
||
|
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
|
||
|
}
|
||
|
let safeKey;
|
||
|
let block;
|
||
|
switch (typeof value) {
|
||
|
case "object":
|
||
|
if (value instanceof Markup) {
|
||
|
safeKey = `string_safe`;
|
||
|
block = html(value);
|
||
|
}
|
||
|
else if (value instanceof LazyValue) {
|
||
|
safeKey = `lazy_value`;
|
||
|
block = value.evaluate();
|
||
|
}
|
||
|
else if (value instanceof String) {
|
||
|
safeKey = "string_unsafe";
|
||
|
block = text(value);
|
||
|
}
|
||
|
else {
|
||
|
// Assuming it is a block
|
||
|
safeKey = "block_safe";
|
||
|
block = value;
|
||
|
}
|
||
|
break;
|
||
|
case "string":
|
||
|
safeKey = "string_unsafe";
|
||
|
block = text(value);
|
||
|
break;
|
||
|
default:
|
||
|
safeKey = "string_unsafe";
|
||
|
block = text(String(value));
|
||
|
}
|
||
|
return toggler(safeKey, block);
|
||
|
}
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
function validateProps(name, props, comp) {
|
||
|
const ComponentClass = typeof name !== "string"
|
||
|
? name
|
||
|
: comp.constructor.components[name];
|
||
|
if (!ComponentClass) {
|
||
|
// this is an error, wrong component. We silently return here instead so the
|
||
|
// error is triggered by the usual path ('component' function)
|
||
|
return;
|
||
|
}
|
||
|
const schema = ComponentClass.props;
|
||
|
if (!schema) {
|
||
|
if (comp.__owl__.app.warnIfNoStaticProps) {
|
||
|
console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
const defaultProps = ComponentClass.defaultProps;
|
||
|
if (defaultProps) {
|
||
|
let isMandatory = (name) => Array.isArray(schema)
|
||
|
? schema.includes(name)
|
||
|
: name in schema && !("*" in schema) && !isOptional(schema[name]);
|
||
|
for (let p in defaultProps) {
|
||
|
if (isMandatory(p)) {
|
||
|
throw new OwlError(`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const errors = validateSchema(props, schema);
|
||
|
if (errors.length) {
|
||
|
throw new OwlError(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
|
||
|
}
|
||
|
}
|
||
|
function makeRefWrapper(node) {
|
||
|
let refNames = new Set();
|
||
|
return (name, fn) => {
|
||
|
if (refNames.has(name)) {
|
||
|
throw new OwlError(`Cannot set the same ref more than once in the same component, ref "${name}" was set multiple times in ${node.name}`);
|
||
|
}
|
||
|
refNames.add(name);
|
||
|
return fn;
|
||
|
};
|
||
|
}
|
||
|
const helpers = {
|
||
|
withDefault,
|
||
|
zero: Symbol("zero"),
|
||
|
isBoundary,
|
||
|
callSlot,
|
||
|
capture,
|
||
|
withKey,
|
||
|
prepareList,
|
||
|
setContextValue,
|
||
|
shallowEqual,
|
||
|
toNumber,
|
||
|
validateProps,
|
||
|
LazyValue,
|
||
|
safeOutput,
|
||
|
createCatcher,
|
||
|
markRaw,
|
||
|
OwlError,
|
||
|
makeRefWrapper,
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Parses an XML string into an XML document, throwing errors on parser errors
|
||
|
* instead of returning an XML document containing the parseerror.
|
||
|
*
|
||
|
* @param xml the string to parse
|
||
|
* @returns an XML document corresponding to the content of the string
|
||
|
*/
|
||
|
function parseXML(xml) {
|
||
|
const parser = new DOMParser();
|
||
|
const doc = parser.parseFromString(xml, "text/xml");
|
||
|
if (doc.getElementsByTagName("parsererror").length) {
|
||
|
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 OwlError(msg);
|
||
|
}
|
||
|
return doc;
|
||
|
}
|
||
|
|
||
|
const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||
|
class TemplateSet {
|
||
|
constructor(config = {}) {
|
||
|
this.rawTemplates = Object.create(globalTemplates);
|
||
|
this.templates = {};
|
||
|
this.Portal = Portal;
|
||
|
this.dev = config.dev || false;
|
||
|
this.translateFn = config.translateFn;
|
||
|
this.translatableAttributes = config.translatableAttributes;
|
||
|
if (config.templates) {
|
||
|
if (config.templates instanceof Document || typeof config.templates === "string") {
|
||
|
this.addTemplates(config.templates);
|
||
|
}
|
||
|
else {
|
||
|
for (const name in config.templates) {
|
||
|
this.addTemplate(name, config.templates[name]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.getRawTemplate = config.getTemplate;
|
||
|
this.customDirectives = config.customDirectives || {};
|
||
|
this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };
|
||
|
this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);
|
||
|
}
|
||
|
static registerTemplate(name, fn) {
|
||
|
globalTemplates[name] = fn;
|
||
|
}
|
||
|
addTemplate(name, template) {
|
||
|
if (name in this.rawTemplates) {
|
||
|
// this check can be expensive, just silently ignore double definitions outside dev mode
|
||
|
if (!this.dev) {
|
||
|
return;
|
||
|
}
|
||
|
const rawTemplate = this.rawTemplates[name];
|
||
|
const currentAsString = typeof rawTemplate === "string"
|
||
|
? rawTemplate
|
||
|
: rawTemplate instanceof Element
|
||
|
? rawTemplate.outerHTML
|
||
|
: rawTemplate.toString();
|
||
|
const newAsString = typeof template === "string" ? template : template.outerHTML;
|
||
|
if (currentAsString === newAsString) {
|
||
|
return;
|
||
|
}
|
||
|
throw new OwlError(`Template ${name} already defined with different content`);
|
||
|
}
|
||
|
this.rawTemplates[name] = template;
|
||
|
}
|
||
|
addTemplates(xml) {
|
||
|
if (!xml) {
|
||
|
// empty string
|
||
|
return;
|
||
|
}
|
||
|
xml = xml instanceof Document ? xml : parseXML(xml);
|
||
|
for (const template of xml.querySelectorAll("[t-name]")) {
|
||
|
const name = template.getAttribute("t-name");
|
||
|
this.addTemplate(name, template);
|
||
|
}
|
||
|
}
|
||
|
getTemplate(name) {
|
||
|
var _a;
|
||
|
if (!(name in this.templates)) {
|
||
|
const rawTemplate = ((_a = this.getRawTemplate) === null || _a === void 0 ? void 0 : _a.call(this, name)) || this.rawTemplates[name];
|
||
|
if (rawTemplate === undefined) {
|
||
|
let extraInfo = "";
|
||
|
try {
|
||
|
const componentName = getCurrent().component.constructor.name;
|
||
|
extraInfo = ` (for component "${componentName}")`;
|
||
|
}
|
||
|
catch { }
|
||
|
throw new OwlError(`Missing template: "${name}"${extraInfo}`);
|
||
|
}
|
||
|
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
|
||
|
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
|
||
|
// first add a function to lazily get the template, in case there is a
|
||
|
// recursive call to the template name
|
||
|
const templates = this.templates;
|
||
|
this.templates[name] = function (context, parent) {
|
||
|
return templates[name].call(this, context, parent);
|
||
|
};
|
||
|
const template = templateFn(this, bdom, this.runtimeUtils);
|
||
|
this.templates[name] = template;
|
||
|
}
|
||
|
return this.templates[name];
|
||
|
}
|
||
|
_compileTemplate(name, template) {
|
||
|
throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
|
||
|
}
|
||
|
callTemplate(owner, subTemplate, ctx, parent, key) {
|
||
|
const template = this.getTemplate(subTemplate);
|
||
|
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
|
||
|
}
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// xml tag helper
|
||
|
// -----------------------------------------------------------------------------
|
||
|
const globalTemplates = {};
|
||
|
function xml(...args) {
|
||
|
const name = `__template__${xml.nextId++}`;
|
||
|
const value = String.raw(...args);
|
||
|
globalTemplates[name] = value;
|
||
|
return name;
|
||
|
}
|
||
|
xml.nextId = 1;
|
||
|
TemplateSet.registerTemplate("__portal__", portalTemplate);
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
* <t t-if="computeSomething({val: state.val})">...</t>
|
||
|
* ```
|
||
|
*
|
||
|
* 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,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(",");
|
||
|
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
|
||
|
and: "&&",
|
||
|
or: "||",
|
||
|
gt: ">",
|
||
|
gte: ">=",
|
||
|
lt: "<",
|
||
|
lte: "<=",
|
||
|
});
|
||
|
const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
|
||
|
"{": "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. Currently we don't support delete and void
|
||
|
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
|
||
|
let tokenizeString = function (expr) {
|
||
|
let s = expr[0];
|
||
|
let start = s;
|
||
|
if (s !== "'" && 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 OwlError("Invalid expression");
|
||
|
}
|
||
|
s += cur;
|
||
|
}
|
||
|
i++;
|
||
|
}
|
||
|
if (expr[i] !== start) {
|
||
|
throw new OwlError("Invalid expression");
|
||
|
}
|
||
|
s += start;
|
||
|
if (start === "`") {
|
||
|
return {
|
||
|
type: "TEMPLATE_STRING",
|
||
|
value: s,
|
||
|
replace(replacer) {
|
||
|
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
|
||
|
return "${" + replacer(group) + "}";
|
||
|
});
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
return { type: "VALUE", value: s };
|
||
|
};
|
||
|
let tokenizeNumber = function (expr) {
|
||
|
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;
|
||
|
let error;
|
||
|
let current = expr;
|
||
|
try {
|
||
|
while (token) {
|
||
|
current = current.trim();
|
||
|
if (current) {
|
||
|
for (let tokenizer of TOKENIZERS) {
|
||
|
token = tokenizer(current);
|
||
|
if (token) {
|
||
|
result.push(token);
|
||
|
current = current.slice(token.size || token.value.length);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
token = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (e) {
|
||
|
error = e; // Silence all errors and throw a generic error below
|
||
|
}
|
||
|
if (current.length || error) {
|
||
|
throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
//------------------------------------------------------------------------------
|
||
|
// Expression "evaluator"
|
||
|
//------------------------------------------------------------------------------
|
||
|
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
|
||
|
const isRightSeparator = (token) => token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
|
||
|
/**
|
||
|
* This is the main function exported by this file. This is the code that will
|
||
|
* process an expression (given as a string) and returns another expression with
|
||
|
* 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}`)
|
||
|
*
|
||
|
* Some specific code is also required to support arrow functions. If we detect
|
||
|
* the arrow operator, then we add the current (or some previous tokens) token to
|
||
|
* the list of variables so it does not get replaced by a lookup in the context
|
||
|
*/
|
||
|
function compileExprToArray(expr) {
|
||
|
const localVars = new Set();
|
||
|
const tokens = tokenize(expr);
|
||
|
let i = 0;
|
||
|
let stack = []; // to track last opening (, [ or {
|
||
|
while (i < tokens.length) {
|
||
|
let token = tokens[i];
|
||
|
let prevToken = tokens[i - 1];
|
||
|
let nextToken = tokens[i + 1];
|
||
|
let groupType = stack[stack.length - 1];
|
||
|
switch (token.type) {
|
||
|
case "LEFT_BRACE":
|
||
|
case "LEFT_BRACKET":
|
||
|
case "LEFT_PAREN":
|
||
|
stack.push(token.type);
|
||
|
break;
|
||
|
case "RIGHT_BRACE":
|
||
|
case "RIGHT_BRACKET":
|
||
|
case "RIGHT_PAREN":
|
||
|
stack.pop();
|
||
|
}
|
||
|
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||
|
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||
|
if (prevToken) {
|
||
|
// normalize missing tokens: {a} should be equivalent to {a:a}
|
||
|
if (groupType === "LEFT_BRACE" &&
|
||
|
isLeftSeparator(prevToken) &&
|
||
|
isRightSeparator(nextToken)) {
|
||
|
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
|
||
|
nextToken = tokens[i + 1];
|
||
|
}
|
||
|
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||
|
isVar = false;
|
||
|
}
|
||
|
else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
|
||
|
if (nextToken && nextToken.type === "COLON") {
|
||
|
isVar = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (token.type === "TEMPLATE_STRING") {
|
||
|
token.value = token.replace((expr) => compileExpr(expr));
|
||
|
}
|
||
|
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
|
||
|
if (token.type === "RIGHT_PAREN") {
|
||
|
let j = i - 1;
|
||
|
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
|
||
|
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
|
||
|
tokens[j].value = tokens[j].originalValue;
|
||
|
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
|
||
|
}
|
||
|
j--;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
localVars.add(token.value); //] = { id: token.value, expr: token.value };
|
||
|
}
|
||
|
}
|
||
|
if (isVar) {
|
||
|
token.varName = token.value;
|
||
|
if (!localVars.has(token.value)) {
|
||
|
token.originalValue = token.value;
|
||
|
token.value = `ctx['${token.value}']`;
|
||
|
}
|
||
|
}
|
||
|
i++;
|
||
|
}
|
||
|
// Mark all variables that have been used locally.
|
||
|
// This assumes the expression has only one scope (incorrect but "good enough for now")
|
||
|
for (const token of tokens) {
|
||
|
if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) {
|
||
|
token.originalValue = token.value;
|
||
|
token.value = `_${token.value}`;
|
||
|
token.isLocal = true;
|
||
|
}
|
||
|
}
|
||
|
return tokens;
|
||
|
}
|
||
|
// Leading spaces are trimmed during tokenization, so they need to be added back for some values
|
||
|
const paddedValues = new Map([["in ", " in "]]);
|
||
|
function compileExpr(expr) {
|
||
|
return compileExprToArray(expr)
|
||
|
.map((t) => paddedValues.get(t.value) || t.value)
|
||
|
.join("");
|
||
|
}
|
||
|
const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
|
||
|
function replaceDynamicParts(s, replacer) {
|
||
|
let matches = s.match(INTERP_REGEXP);
|
||
|
if (matches && matches[0].length === s.length) {
|
||
|
return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
|
||
|
}
|
||
|
let r = s.replace(INTERP_REGEXP, (s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}");
|
||
|
return "`" + r + "`";
|
||
|
}
|
||
|
function interpolate(s) {
|
||
|
return replaceDynamicParts(s, compileExpr);
|
||
|
}
|
||
|
|
||
|
const whitespaceRE = /\s+/g;
|
||
|
// using a non-html document so that <inner/outer>HTML serializes as XML instead
|
||
|
// of HTML (as we will parse it as xml later)
|
||
|
const xmlDoc = document.implementation.createDocument(null, null, null);
|
||
|
const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
|
||
|
let nextDataIds = {};
|
||
|
function generateId(prefix = "") {
|
||
|
nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
|
||
|
return prefix + nextDataIds[prefix];
|
||
|
}
|
||
|
function isProp(tag, key) {
|
||
|
switch (tag) {
|
||
|
case "input":
|
||
|
return (key === "checked" ||
|
||
|
key === "indeterminate" ||
|
||
|
key === "value" ||
|
||
|
key === "readonly" ||
|
||
|
key === "readOnly" ||
|
||
|
key === "disabled");
|
||
|
case "option":
|
||
|
return key === "selected" || key === "disabled";
|
||
|
case "textarea":
|
||
|
return key === "value" || key === "readonly" || key === "readOnly" || key === "disabled";
|
||
|
case "select":
|
||
|
return key === "value" || key === "disabled";
|
||
|
case "button":
|
||
|
case "optgroup":
|
||
|
return key === "disabled";
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
/**
|
||
|
* Returns a template literal that evaluates to str. You can add interpolation
|
||
|
* sigils into the string if required
|
||
|
*/
|
||
|
function toStringExpression(str) {
|
||
|
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// BlockDescription
|
||
|
// -----------------------------------------------------------------------------
|
||
|
class BlockDescription {
|
||
|
constructor(target, type) {
|
||
|
this.dynamicTagName = null;
|
||
|
this.isRoot = false;
|
||
|
this.hasDynamicChildren = false;
|
||
|
this.children = [];
|
||
|
this.data = [];
|
||
|
this.childNumber = 0;
|
||
|
this.parentVar = "";
|
||
|
this.id = BlockDescription.nextBlockId++;
|
||
|
this.varName = "b" + this.id;
|
||
|
this.blockName = "block" + this.id;
|
||
|
this.target = target;
|
||
|
this.type = type;
|
||
|
}
|
||
|
insertData(str, prefix = "d") {
|
||
|
const id = generateId(prefix);
|
||
|
this.target.addLine(`let ${id} = ${str};`);
|
||
|
return this.data.push(id) - 1;
|
||
|
}
|
||
|
insert(dom) {
|
||
|
if (this.currentDom) {
|
||
|
this.currentDom.appendChild(dom);
|
||
|
}
|
||
|
else {
|
||
|
this.dom = dom;
|
||
|
}
|
||
|
}
|
||
|
generateExpr(expr) {
|
||
|
if (this.type === "block") {
|
||
|
const hasChildren = this.children.length;
|
||
|
let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : "";
|
||
|
if (hasChildren) {
|
||
|
params += ", [" + this.children.map((c) => c.varName).join(", ") + "]";
|
||
|
}
|
||
|
if (this.dynamicTagName) {
|
||
|
return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;
|
||
|
}
|
||
|
return `${this.blockName}(${params})`;
|
||
|
}
|
||
|
else if (this.type === "list") {
|
||
|
return `list(c_block${this.id})`;
|
||
|
}
|
||
|
return expr;
|
||
|
}
|
||
|
asXmlString() {
|
||
|
// Can't use outerHTML on text/comment nodes
|
||
|
// append dom to any element and use innerHTML instead
|
||
|
const t = xmlDoc.createElement("t");
|
||
|
t.appendChild(this.dom);
|
||
|
return t.innerHTML;
|
||
|
}
|
||
|
}
|
||
|
BlockDescription.nextBlockId = 1;
|
||
|
function createContext(parentCtx, params) {
|
||
|
return Object.assign({
|
||
|
block: null,
|
||
|
index: 0,
|
||
|
forceNewBlock: true,
|
||
|
translate: parentCtx.translate,
|
||
|
translationCtx: parentCtx.translationCtx,
|
||
|
tKeyExpr: null,
|
||
|
nameSpace: parentCtx.nameSpace,
|
||
|
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||
|
}, params);
|
||
|
}
|
||
|
class CodeTarget {
|
||
|
constructor(name, on) {
|
||
|
this.indentLevel = 0;
|
||
|
this.loopLevel = 0;
|
||
|
this.code = [];
|
||
|
this.hasRoot = false;
|
||
|
this.hasCache = false;
|
||
|
this.shouldProtectScope = false;
|
||
|
this.hasRefWrapper = false;
|
||
|
this.name = name;
|
||
|
this.on = on || null;
|
||
|
}
|
||
|
addLine(line, idx) {
|
||
|
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||
|
if (idx === undefined) {
|
||
|
this.code.push(prefix + line);
|
||
|
}
|
||
|
else {
|
||
|
this.code.splice(idx, 0, prefix + line);
|
||
|
}
|
||
|
}
|
||
|
generateCode() {
|
||
|
let result = [];
|
||
|
result.push(`function ${this.name}(ctx, node, key = "") {`);
|
||
|
if (this.shouldProtectScope) {
|
||
|
result.push(` ctx = Object.create(ctx);`);
|
||
|
result.push(` ctx[isBoundary] = 1`);
|
||
|
}
|
||
|
if (this.hasRefWrapper) {
|
||
|
result.push(` let refWrapper = makeRefWrapper(this.__owl__);`);
|
||
|
}
|
||
|
if (this.hasCache) {
|
||
|
result.push(` let cache = ctx.cache || {};`);
|
||
|
result.push(` let nextCache = ctx.cache = {};`);
|
||
|
}
|
||
|
for (let line of this.code) {
|
||
|
result.push(line);
|
||
|
}
|
||
|
if (!this.hasRoot) {
|
||
|
result.push(`return text('');`);
|
||
|
}
|
||
|
result.push(`}`);
|
||
|
return result.join("\n ");
|
||
|
}
|
||
|
currentKey(ctx) {
|
||
|
let key = this.loopLevel ? `key${this.loopLevel}` : "key";
|
||
|
if (ctx.tKeyExpr) {
|
||
|
key = `${ctx.tKeyExpr} + ${key}`;
|
||
|
}
|
||
|
return key;
|
||
|
}
|
||
|
}
|
||
|
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||
|
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||
|
class CodeGenerator {
|
||
|
constructor(ast, options) {
|
||
|
this.blocks = [];
|
||
|
this.nextBlockId = 1;
|
||
|
this.isDebug = false;
|
||
|
this.targets = [];
|
||
|
this.target = new CodeTarget("template");
|
||
|
this.translatableAttributes = TRANSLATABLE_ATTRS;
|
||
|
this.staticDefs = [];
|
||
|
this.slotNames = new Set();
|
||
|
this.helpers = new Set();
|
||
|
this.translateFn = options.translateFn || ((s) => s);
|
||
|
if (options.translatableAttributes) {
|
||
|
const attrs = new Set(TRANSLATABLE_ATTRS);
|
||
|
for (let attr of options.translatableAttributes) {
|
||
|
if (attr.startsWith("-")) {
|
||
|
attrs.delete(attr.slice(1));
|
||
|
}
|
||
|
else {
|
||
|
attrs.add(attr);
|
||
|
}
|
||
|
}
|
||
|
this.translatableAttributes = [...attrs];
|
||
|
}
|
||
|
this.hasSafeContext = options.hasSafeContext || false;
|
||
|
this.dev = options.dev || false;
|
||
|
this.ast = ast;
|
||
|
this.templateName = options.name;
|
||
|
if (options.hasGlobalValues) {
|
||
|
this.helpers.add("__globals__");
|
||
|
}
|
||
|
}
|
||
|
generateCode() {
|
||
|
const ast = this.ast;
|
||
|
this.isDebug = ast.type === 12 /* TDebug */;
|
||
|
BlockDescription.nextBlockId = 1;
|
||
|
nextDataIds = {};
|
||
|
this.compileAST(ast, {
|
||
|
block: null,
|
||
|
index: 0,
|
||
|
forceNewBlock: false,
|
||
|
isLast: true,
|
||
|
translate: true,
|
||
|
translationCtx: "",
|
||
|
tKeyExpr: null,
|
||
|
});
|
||
|
// define blocks and utility functions
|
||
|
let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
|
||
|
if (this.helpers.size) {
|
||
|
mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
|
||
|
}
|
||
|
if (this.templateName) {
|
||
|
mainCode.push(`// Template name: "${this.templateName}"`);
|
||
|
}
|
||
|
for (let { id, expr } of this.staticDefs) {
|
||
|
mainCode.push(`const ${id} = ${expr};`);
|
||
|
}
|
||
|
// define all blocks
|
||
|
if (this.blocks.length) {
|
||
|
mainCode.push(``);
|
||
|
for (let block of this.blocks) {
|
||
|
if (block.dom) {
|
||
|
let xmlString = toStringExpression(block.asXmlString());
|
||
|
if (block.dynamicTagName) {
|
||
|
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||
|
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||
|
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||
|
}
|
||
|
else {
|
||
|
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// define all slots/defaultcontent function
|
||
|
if (this.targets.length) {
|
||
|
for (let fn of this.targets) {
|
||
|
mainCode.push("");
|
||
|
mainCode = mainCode.concat(fn.generateCode());
|
||
|
}
|
||
|
}
|
||
|
// generate main code
|
||
|
mainCode.push("");
|
||
|
mainCode = mainCode.concat("return " + this.target.generateCode());
|
||
|
const code = mainCode.join("\n ");
|
||
|
if (this.isDebug) {
|
||
|
const msg = `[Owl Debug]\n${code}`;
|
||
|
console.log(msg);
|
||
|
}
|
||
|
return code;
|
||
|
}
|
||
|
compileInNewTarget(prefix, ast, ctx, on) {
|
||
|
const name = generateId(prefix);
|
||
|
const initialTarget = this.target;
|
||
|
const target = new CodeTarget(name, on);
|
||
|
this.targets.push(target);
|
||
|
this.target = target;
|
||
|
this.compileAST(ast, createContext(ctx));
|
||
|
this.target = initialTarget;
|
||
|
return name;
|
||
|
}
|
||
|
addLine(line, idx) {
|
||
|
this.target.addLine(line, idx);
|
||
|
}
|
||
|
define(varName, expr) {
|
||
|
this.addLine(`const ${varName} = ${expr};`);
|
||
|
}
|
||
|
insertAnchor(block, index = block.children.length) {
|
||
|
const tag = `block-child-${index}`;
|
||
|
const anchor = xmlDoc.createElement(tag);
|
||
|
block.insert(anchor);
|
||
|
}
|
||
|
createBlock(parentBlock, type, ctx) {
|
||
|
const hasRoot = this.target.hasRoot;
|
||
|
const block = new BlockDescription(this.target, type);
|
||
|
if (!hasRoot) {
|
||
|
this.target.hasRoot = true;
|
||
|
block.isRoot = true;
|
||
|
}
|
||
|
if (parentBlock) {
|
||
|
parentBlock.children.push(block);
|
||
|
if (parentBlock.type === "list") {
|
||
|
block.parentVar = `c_block${parentBlock.id}`;
|
||
|
}
|
||
|
}
|
||
|
return block;
|
||
|
}
|
||
|
insertBlock(expression, block, ctx) {
|
||
|
let blockExpr = block.generateExpr(expression);
|
||
|
if (block.parentVar) {
|
||
|
let key = this.target.currentKey(ctx);
|
||
|
this.helpers.add("withKey");
|
||
|
this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);
|
||
|
return;
|
||
|
}
|
||
|
if (ctx.tKeyExpr) {
|
||
|
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
|
||
|
}
|
||
|
if (block.isRoot) {
|
||
|
if (this.target.on) {
|
||
|
blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
|
||
|
}
|
||
|
this.addLine(`return ${blockExpr};`);
|
||
|
}
|
||
|
else {
|
||
|
this.define(block.varName, blockExpr);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Captures variables that are used inside of an expression. This is useful
|
||
|
* because in compiled code, almost all variables are accessed through the ctx
|
||
|
* object. In the case of functions, that lookup in the context can be delayed
|
||
|
* which can cause issues if the value has changed since the function was
|
||
|
* defined.
|
||
|
*
|
||
|
* @param expr the expression to capture
|
||
|
* @param forceCapture whether the expression should capture its scope even if
|
||
|
* it doesn't contain a function. Useful when the expression will be used as
|
||
|
* a function body.
|
||
|
* @returns a new expression that uses the captured values
|
||
|
*/
|
||
|
captureExpression(expr, forceCapture = false) {
|
||
|
if (!forceCapture && !expr.includes("=>")) {
|
||
|
return compileExpr(expr);
|
||
|
}
|
||
|
const tokens = compileExprToArray(expr);
|
||
|
const mapping = new Map();
|
||
|
return tokens
|
||
|
.map((tok) => {
|
||
|
if (tok.varName && !tok.isLocal) {
|
||
|
if (!mapping.has(tok.varName)) {
|
||
|
const varId = generateId("v");
|
||
|
mapping.set(tok.varName, varId);
|
||
|
this.define(varId, tok.value);
|
||
|
}
|
||
|
tok.value = mapping.get(tok.varName);
|
||
|
}
|
||
|
return tok.value;
|
||
|
})
|
||
|
.join("");
|
||
|
}
|
||
|
translate(str, translationCtx) {
|
||
|
const match = translationRE.exec(str);
|
||
|
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
|
||
|
}
|
||
|
/**
|
||
|
* @returns the newly created block name, if any
|
||
|
*/
|
||
|
compileAST(ast, ctx) {
|
||
|
switch (ast.type) {
|
||
|
case 1 /* Comment */:
|
||
|
return this.compileComment(ast, ctx);
|
||
|
case 0 /* Text */:
|
||
|
return this.compileText(ast, ctx);
|
||
|
case 2 /* DomNode */:
|
||
|
return this.compileTDomNode(ast, ctx);
|
||
|
case 4 /* TEsc */:
|
||
|
return this.compileTEsc(ast, ctx);
|
||
|
case 8 /* TOut */:
|
||
|
return this.compileTOut(ast, ctx);
|
||
|
case 5 /* TIf */:
|
||
|
return this.compileTIf(ast, ctx);
|
||
|
case 9 /* TForEach */:
|
||
|
return this.compileTForeach(ast, ctx);
|
||
|
case 10 /* TKey */:
|
||
|
return this.compileTKey(ast, ctx);
|
||
|
case 3 /* Multi */:
|
||
|
return this.compileMulti(ast, ctx);
|
||
|
case 7 /* TCall */:
|
||
|
return this.compileTCall(ast, ctx);
|
||
|
case 15 /* TCallBlock */:
|
||
|
return this.compileTCallBlock(ast, ctx);
|
||
|
case 6 /* TSet */:
|
||
|
return this.compileTSet(ast, ctx);
|
||
|
case 11 /* TComponent */:
|
||
|
return this.compileComponent(ast, ctx);
|
||
|
case 12 /* TDebug */:
|
||
|
return this.compileDebug(ast, ctx);
|
||
|
case 13 /* TLog */:
|
||
|
return this.compileLog(ast, ctx);
|
||
|
case 14 /* TSlot */:
|
||
|
return this.compileTSlot(ast, ctx);
|
||
|
case 16 /* TTranslation */:
|
||
|
return this.compileTTranslation(ast, ctx);
|
||
|
case 17 /* TTranslationContext */:
|
||
|
return this.compileTTranslationContext(ast, ctx);
|
||
|
case 18 /* TPortal */:
|
||
|
return this.compileTPortal(ast, ctx);
|
||
|
}
|
||
|
}
|
||
|
compileDebug(ast, ctx) {
|
||
|
this.addLine(`debugger;`);
|
||
|
if (ast.content) {
|
||
|
return this.compileAST(ast.content, ctx);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
compileLog(ast, ctx) {
|
||
|
this.addLine(`console.log(${compileExpr(ast.expr)});`);
|
||
|
if (ast.content) {
|
||
|
return this.compileAST(ast.content, ctx);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
compileComment(ast, ctx) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
const isNewBlock = !block || forceNewBlock;
|
||
|
if (isNewBlock) {
|
||
|
block = this.createBlock(block, "comment", ctx);
|
||
|
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||
|
...ctx,
|
||
|
forceNewBlock: forceNewBlock && !block,
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
const text = xmlDoc.createComment(ast.value);
|
||
|
block.insert(text);
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileText(ast, ctx) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
let value = ast.value;
|
||
|
if (value && ctx.translate !== false) {
|
||
|
value = this.translate(value, ctx.translationCtx);
|
||
|
}
|
||
|
if (!ctx.inPreTag) {
|
||
|
value = value.replace(whitespaceRE, " ");
|
||
|
}
|
||
|
if (!block || forceNewBlock) {
|
||
|
block = this.createBlock(block, "text", ctx);
|
||
|
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||
|
...ctx,
|
||
|
forceNewBlock: forceNewBlock && !block,
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;
|
||
|
block.insert(createFn.call(xmlDoc, value));
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
generateHandlerCode(rawEvent, handler) {
|
||
|
const modifiers = rawEvent
|
||
|
.split(".")
|
||
|
.slice(1)
|
||
|
.map((m) => {
|
||
|
if (!MODS.has(m)) {
|
||
|
throw new OwlError(`Unknown event modifier: '${m}'`);
|
||
|
}
|
||
|
return `"${m}"`;
|
||
|
});
|
||
|
let modifiersCode = "";
|
||
|
if (modifiers.length) {
|
||
|
modifiersCode = `${modifiers.join(",")}, `;
|
||
|
}
|
||
|
return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
|
||
|
}
|
||
|
compileTDomNode(ast, ctx) {
|
||
|
var _a;
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
|
||
|
let codeIdx = this.target.code.length;
|
||
|
if (isNewBlock) {
|
||
|
if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {
|
||
|
this.insertAnchor(ctx.block);
|
||
|
}
|
||
|
block = this.createBlock(block, "block", ctx);
|
||
|
this.blocks.push(block);
|
||
|
if (ast.dynamicTag) {
|
||
|
const tagExpr = generateId("tag");
|
||
|
this.define(tagExpr, compileExpr(ast.dynamicTag));
|
||
|
block.dynamicTagName = tagExpr;
|
||
|
}
|
||
|
}
|
||
|
// attributes
|
||
|
const attrs = {};
|
||
|
for (let key in ast.attrs) {
|
||
|
let expr, attrName;
|
||
|
if (key.startsWith("t-attf")) {
|
||
|
expr = interpolate(ast.attrs[key]);
|
||
|
const idx = block.insertData(expr, "attr");
|
||
|
attrName = key.slice(7);
|
||
|
attrs["block-attribute-" + idx] = attrName;
|
||
|
}
|
||
|
else if (key.startsWith("t-att")) {
|
||
|
attrName = key === "t-att" ? null : key.slice(6);
|
||
|
expr = compileExpr(ast.attrs[key]);
|
||
|
if (attrName && isProp(ast.tag, attrName)) {
|
||
|
if (attrName === "readonly") {
|
||
|
// the property has a different name than the attribute
|
||
|
attrName = "readOnly";
|
||
|
}
|
||
|
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
|
||
|
if (attrName === "value") {
|
||
|
// When the expression is falsy (except 0), fall back to an empty string
|
||
|
expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || ""))`;
|
||
|
}
|
||
|
else {
|
||
|
expr = `new Boolean(${expr})`;
|
||
|
}
|
||
|
const idx = block.insertData(expr, "prop");
|
||
|
attrs[`block-property-${idx}`] = attrName;
|
||
|
}
|
||
|
else {
|
||
|
const idx = block.insertData(expr, "attr");
|
||
|
if (key === "t-att") {
|
||
|
attrs[`block-attributes`] = String(idx);
|
||
|
}
|
||
|
else {
|
||
|
attrs[`block-attribute-${idx}`] = attrName;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (this.translatableAttributes.includes(key)) {
|
||
|
const attrTranslationCtx = ((_a = ast.attrsTranslationCtx) === null || _a === void 0 ? void 0 : _a[key]) || ctx.translationCtx;
|
||
|
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
|
||
|
}
|
||
|
else {
|
||
|
expr = `"${ast.attrs[key]}"`;
|
||
|
attrName = key;
|
||
|
attrs[key] = ast.attrs[key];
|
||
|
}
|
||
|
if (attrName === "value" && ctx.tModelSelectedExpr) {
|
||
|
let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr");
|
||
|
attrs[`block-attribute-${selectedId}`] = "selected";
|
||
|
}
|
||
|
}
|
||
|
// t-model
|
||
|
let tModelSelectedExpr;
|
||
|
if (ast.model) {
|
||
|
const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
|
||
|
const baseExpression = compileExpr(baseExpr);
|
||
|
const bExprId = generateId("bExpr");
|
||
|
this.define(bExprId, baseExpression);
|
||
|
const expression = compileExpr(expr);
|
||
|
const exprId = generateId("expr");
|
||
|
this.define(exprId, expression);
|
||
|
const fullExpression = `${bExprId}[${exprId}]`;
|
||
|
let idx;
|
||
|
if (specialInitTargetAttr) {
|
||
|
let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
|
||
|
if (!targetExpr && ast.attrs) {
|
||
|
// look at the dynamic attribute counterpart
|
||
|
const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
|
||
|
if (dynamicTgExpr) {
|
||
|
targetExpr = compileExpr(dynamicTgExpr);
|
||
|
}
|
||
|
}
|
||
|
idx = block.insertData(`${fullExpression} === ${targetExpr}`, "prop");
|
||
|
attrs[`block-property-${idx}`] = specialInitTargetAttr;
|
||
|
}
|
||
|
else if (hasDynamicChildren) {
|
||
|
const bValueId = generateId("bValue");
|
||
|
tModelSelectedExpr = `${bValueId}`;
|
||
|
this.define(tModelSelectedExpr, fullExpression);
|
||
|
}
|
||
|
else {
|
||
|
idx = block.insertData(`${fullExpression}`, "prop");
|
||
|
attrs[`block-property-${idx}`] = targetAttr;
|
||
|
}
|
||
|
this.helpers.add("toNumber");
|
||
|
let valueCode = `ev.target.${targetAttr}`;
|
||
|
valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;
|
||
|
valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;
|
||
|
const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`;
|
||
|
idx = block.insertData(handler, "hdlr");
|
||
|
attrs[`block-handler-${idx}`] = eventType;
|
||
|
}
|
||
|
// event handlers
|
||
|
for (let ev in ast.on) {
|
||
|
const name = this.generateHandlerCode(ev, ast.on[ev]);
|
||
|
const idx = block.insertData(name, "hdlr");
|
||
|
attrs[`block-handler-${idx}`] = ev;
|
||
|
}
|
||
|
// t-ref
|
||
|
if (ast.ref) {
|
||
|
if (this.dev) {
|
||
|
this.helpers.add("makeRefWrapper");
|
||
|
this.target.hasRefWrapper = true;
|
||
|
}
|
||
|
const isDynamic = INTERP_REGEXP.test(ast.ref);
|
||
|
let name = `\`${ast.ref}\``;
|
||
|
if (isDynamic) {
|
||
|
name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
|
||
|
}
|
||
|
let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;
|
||
|
if (this.dev) {
|
||
|
setRefStr = `refWrapper(${name}, ${setRefStr})`;
|
||
|
}
|
||
|
const idx = block.insertData(setRefStr, "ref");
|
||
|
attrs["block-ref"] = String(idx);
|
||
|
}
|
||
|
const nameSpace = ast.ns || ctx.nameSpace;
|
||
|
const dom = nameSpace
|
||
|
? xmlDoc.createElementNS(nameSpace, ast.tag)
|
||
|
: xmlDoc.createElement(ast.tag);
|
||
|
for (const [attr, val] of Object.entries(attrs)) {
|
||
|
if (!(attr === "class" && val === "")) {
|
||
|
dom.setAttribute(attr, val);
|
||
|
}
|
||
|
}
|
||
|
block.insert(dom);
|
||
|
if (ast.content.length) {
|
||
|
const initialDom = block.currentDom;
|
||
|
block.currentDom = dom;
|
||
|
const children = ast.content;
|
||
|
for (let i = 0; i < children.length; i++) {
|
||
|
const child = ast.content[i];
|
||
|
const subCtx = createContext(ctx, {
|
||
|
block,
|
||
|
index: block.childNumber,
|
||
|
forceNewBlock: false,
|
||
|
isLast: ctx.isLast && i === children.length - 1,
|
||
|
tKeyExpr: ctx.tKeyExpr,
|
||
|
nameSpace,
|
||
|
tModelSelectedExpr,
|
||
|
inPreTag: ctx.inPreTag || ast.tag === "pre",
|
||
|
});
|
||
|
this.compileAST(child, subCtx);
|
||
|
}
|
||
|
block.currentDom = initialDom;
|
||
|
}
|
||
|
if (isNewBlock) {
|
||
|
this.insertBlock(`${block.blockName}(ddd)`, block, ctx);
|
||
|
// may need to rewrite code!
|
||
|
if (block.children.length && block.hasDynamicChildren) {
|
||
|
const code = this.target.code;
|
||
|
const children = block.children.slice();
|
||
|
let current = children.shift();
|
||
|
for (let i = codeIdx; i < code.length; i++) {
|
||
|
if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
|
||
|
code[i] = code[i].replace(`const ${current.varName}`, current.varName);
|
||
|
current = children.shift();
|
||
|
if (!current)
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||
|
}
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTEsc(ast, ctx) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
let expr;
|
||
|
if (ast.expr === "0") {
|
||
|
this.helpers.add("zero");
|
||
|
expr = `ctx[zero]`;
|
||
|
}
|
||
|
else {
|
||
|
expr = compileExpr(ast.expr);
|
||
|
if (ast.defaultValue) {
|
||
|
this.helpers.add("withDefault");
|
||
|
// FIXME: defaultValue is not translated
|
||
|
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||
|
}
|
||
|
}
|
||
|
if (!block || forceNewBlock) {
|
||
|
block = this.createBlock(block, "text", ctx);
|
||
|
this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block });
|
||
|
}
|
||
|
else {
|
||
|
const idx = block.insertData(expr, "txt");
|
||
|
const text = xmlDoc.createElement(`block-text-${idx}`);
|
||
|
block.insert(text);
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTOut(ast, ctx) {
|
||
|
let { block } = ctx;
|
||
|
if (block) {
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
block = this.createBlock(block, "html", ctx);
|
||
|
let blockStr;
|
||
|
if (ast.expr === "0") {
|
||
|
this.helpers.add("zero");
|
||
|
blockStr = `ctx[zero]`;
|
||
|
}
|
||
|
else if (ast.body) {
|
||
|
let bodyValue = null;
|
||
|
bodyValue = BlockDescription.nextBlockId;
|
||
|
const subCtx = createContext(ctx);
|
||
|
this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
|
||
|
this.helpers.add("safeOutput");
|
||
|
blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
|
||
|
}
|
||
|
else {
|
||
|
this.helpers.add("safeOutput");
|
||
|
blockStr = `safeOutput(${compileExpr(ast.expr)})`;
|
||
|
}
|
||
|
this.insertBlock(blockStr, block, ctx);
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTIfBranch(content, block, ctx) {
|
||
|
this.target.indentLevel++;
|
||
|
let childN = block.children.length;
|
||
|
this.compileAST(content, createContext(ctx, { block, index: ctx.index }));
|
||
|
if (block.children.length > childN) {
|
||
|
// we have some content => need to insert an anchor at correct index
|
||
|
this.insertAnchor(block, childN);
|
||
|
}
|
||
|
this.target.indentLevel--;
|
||
|
}
|
||
|
compileTIf(ast, ctx, nextNode) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
const codeIdx = this.target.code.length;
|
||
|
const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
|
||
|
if (block) {
|
||
|
block.hasDynamicChildren = true;
|
||
|
}
|
||
|
if (!block || (block.type !== "multi" && forceNewBlock)) {
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
}
|
||
|
this.addLine(`if (${compileExpr(ast.condition)}) {`);
|
||
|
this.compileTIfBranch(ast.content, block, ctx);
|
||
|
if (ast.tElif) {
|
||
|
for (let clause of ast.tElif) {
|
||
|
this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
|
||
|
this.compileTIfBranch(clause.content, block, ctx);
|
||
|
}
|
||
|
}
|
||
|
if (ast.tElse) {
|
||
|
this.addLine(`} else {`);
|
||
|
this.compileTIfBranch(ast.tElse, block, ctx);
|
||
|
}
|
||
|
this.addLine("}");
|
||
|
if (isNewBlock) {
|
||
|
// note: this part is duplicated from end of compiledomnode:
|
||
|
if (block.children.length) {
|
||
|
const code = this.target.code;
|
||
|
const children = block.children.slice();
|
||
|
let current = children.shift();
|
||
|
for (let i = codeIdx; i < code.length; i++) {
|
||
|
if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
|
||
|
code[i] = code[i].replace(`const ${current.varName}`, current.varName);
|
||
|
current = children.shift();
|
||
|
if (!current)
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||
|
}
|
||
|
// note: this part is duplicated from end of compilemulti:
|
||
|
const args = block.children.map((c) => c.varName).join(", ");
|
||
|
this.insertBlock(`multi([${args}])`, block, ctx);
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTForeach(ast, ctx) {
|
||
|
let { block } = ctx;
|
||
|
if (block) {
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
block = this.createBlock(block, "list", ctx);
|
||
|
this.target.loopLevel++;
|
||
|
const loopVar = `i${this.target.loopLevel}`;
|
||
|
this.addLine(`ctx = Object.create(ctx);`);
|
||
|
const vals = `v_block${block.id}`;
|
||
|
const keys = `k_block${block.id}`;
|
||
|
const l = `l_block${block.id}`;
|
||
|
const c = `c_block${block.id}`;
|
||
|
this.helpers.add("prepareList");
|
||
|
this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
|
||
|
// Throw errors on duplicate keys in dev mode
|
||
|
if (this.dev) {
|
||
|
this.define(`keys${block.id}`, `new Set()`);
|
||
|
}
|
||
|
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
|
||
|
this.target.indentLevel++;
|
||
|
this.addLine(`ctx[\`${ast.elem}\`] = ${keys}[${loopVar}];`);
|
||
|
if (!ast.hasNoFirst) {
|
||
|
this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
|
||
|
}
|
||
|
if (!ast.hasNoLast) {
|
||
|
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${keys}.length - 1;`);
|
||
|
}
|
||
|
if (!ast.hasNoIndex) {
|
||
|
this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
|
||
|
}
|
||
|
if (!ast.hasNoValue) {
|
||
|
this.addLine(`ctx[\`${ast.elem}_value\`] = ${vals}[${loopVar}];`);
|
||
|
}
|
||
|
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
|
||
|
if (this.dev) {
|
||
|
// Throw error on duplicate keys in dev mode
|
||
|
this.helpers.add("OwlError");
|
||
|
this.addLine(`if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
|
||
|
this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);
|
||
|
}
|
||
|
let id;
|
||
|
if (ast.memo) {
|
||
|
this.target.hasCache = true;
|
||
|
id = generateId();
|
||
|
this.define(`memo${id}`, compileExpr(ast.memo));
|
||
|
this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
|
||
|
this.addLine(`if (vnode${id}) {`);
|
||
|
this.target.indentLevel++;
|
||
|
this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
|
||
|
this.target.indentLevel++;
|
||
|
this.addLine(`${c}[${loopVar}] = vnode${id};`);
|
||
|
this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`);
|
||
|
this.addLine(`continue;`);
|
||
|
this.target.indentLevel--;
|
||
|
this.addLine("}");
|
||
|
this.target.indentLevel--;
|
||
|
this.addLine("}");
|
||
|
}
|
||
|
const subCtx = createContext(ctx, { block, index: loopVar });
|
||
|
this.compileAST(ast.body, subCtx);
|
||
|
if (ast.memo) {
|
||
|
this.addLine(`nextCache[key${this.target.loopLevel}] = Object.assign(${c}[${loopVar}], {memo: memo${id}});`);
|
||
|
}
|
||
|
this.target.indentLevel--;
|
||
|
this.target.loopLevel--;
|
||
|
this.addLine(`}`);
|
||
|
if (!ctx.isLast) {
|
||
|
this.addLine(`ctx = ctx.__proto__;`);
|
||
|
}
|
||
|
this.insertBlock("l", block, ctx);
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTKey(ast, ctx) {
|
||
|
const tKeyExpr = generateId("tKey_");
|
||
|
this.define(tKeyExpr, compileExpr(ast.expr));
|
||
|
ctx = createContext(ctx, {
|
||
|
tKeyExpr,
|
||
|
block: ctx.block,
|
||
|
index: ctx.index,
|
||
|
});
|
||
|
return this.compileAST(ast.content, ctx);
|
||
|
}
|
||
|
compileMulti(ast, ctx) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
const isNewBlock = !block || forceNewBlock;
|
||
|
let codeIdx = this.target.code.length;
|
||
|
if (isNewBlock) {
|
||
|
const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;
|
||
|
let result = null;
|
||
|
if (n <= 1) {
|
||
|
for (let child of ast.content) {
|
||
|
const blockName = this.compileAST(child, ctx);
|
||
|
result = result || blockName;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
}
|
||
|
let index = 0;
|
||
|
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||
|
const child = ast.content[i];
|
||
|
const isTSet = child.type === 6 /* TSet */;
|
||
|
const subCtx = createContext(ctx, {
|
||
|
block,
|
||
|
index,
|
||
|
forceNewBlock: !isTSet,
|
||
|
isLast: ctx.isLast && i === l - 1,
|
||
|
});
|
||
|
this.compileAST(child, subCtx);
|
||
|
if (!isTSet) {
|
||
|
index++;
|
||
|
}
|
||
|
}
|
||
|
if (isNewBlock) {
|
||
|
if (block.hasDynamicChildren && block.children.length) {
|
||
|
const code = this.target.code;
|
||
|
const children = block.children.slice();
|
||
|
let current = children.shift();
|
||
|
for (let i = codeIdx; i < code.length; i++) {
|
||
|
if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
|
||
|
code[i] = code[i].replace(`const ${current.varName}`, current.varName);
|
||
|
current = children.shift();
|
||
|
if (!current)
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||
|
}
|
||
|
const args = block.children.map((c) => c.varName).join(", ");
|
||
|
this.insertBlock(`multi([${args}])`, block, ctx);
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTCall(ast, ctx) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
let ctxVar = ctx.ctxVar || "ctx";
|
||
|
if (ast.context) {
|
||
|
ctxVar = generateId("ctx");
|
||
|
this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
|
||
|
}
|
||
|
const isDynamic = INTERP_REGEXP.test(ast.name);
|
||
|
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
||
|
if (block && !forceNewBlock) {
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
if (ast.body) {
|
||
|
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
|
||
|
this.addLine(`${ctxVar}[isBoundary] = 1;`);
|
||
|
this.helpers.add("isBoundary");
|
||
|
const subCtx = createContext(ctx, { ctxVar });
|
||
|
const bl = this.compileMulti({ type: 3 /* Multi */, content: ast.body }, subCtx);
|
||
|
if (bl) {
|
||
|
this.helpers.add("zero");
|
||
|
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
||
|
}
|
||
|
}
|
||
|
const key = this.generateComponentKey();
|
||
|
if (isDynamic) {
|
||
|
const templateVar = generateId("template");
|
||
|
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||
|
this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
|
||
|
}
|
||
|
this.define(templateVar, subTemplate);
|
||
|
this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {
|
||
|
...ctx,
|
||
|
forceNewBlock: !block,
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
const id = generateId(`callTemplate_`);
|
||
|
this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
|
||
|
this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {
|
||
|
...ctx,
|
||
|
forceNewBlock: !block,
|
||
|
});
|
||
|
}
|
||
|
if (ast.body && !ctx.isLast) {
|
||
|
this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);
|
||
|
}
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTCallBlock(ast, ctx) {
|
||
|
let { block, forceNewBlock } = ctx;
|
||
|
if (block) {
|
||
|
if (!forceNewBlock) {
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
}
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTSet(ast, ctx) {
|
||
|
this.target.shouldProtectScope = true;
|
||
|
this.helpers.add("isBoundary").add("withDefault");
|
||
|
const expr = ast.value ? compileExpr(ast.value || "") : "null";
|
||
|
if (ast.body) {
|
||
|
this.helpers.add("LazyValue");
|
||
|
const bodyAst = { type: 3 /* Multi */, content: ast.body };
|
||
|
const name = this.compileInNewTarget("value", bodyAst, ctx);
|
||
|
let key = this.target.currentKey(ctx);
|
||
|
let value = `new LazyValue(${name}, ctx, this, node, ${key})`;
|
||
|
value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
|
||
|
this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
|
||
|
}
|
||
|
else {
|
||
|
let value;
|
||
|
if (ast.defaultValue) {
|
||
|
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue);
|
||
|
if (ast.value) {
|
||
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||
|
}
|
||
|
else {
|
||
|
value = defaultValue;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
value = expr;
|
||
|
}
|
||
|
this.helpers.add("setContextValue");
|
||
|
this.addLine(`setContextValue(${ctx.ctxVar || "ctx"}, "${ast.name}", ${value});`);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
generateComponentKey(currentKey = "key") {
|
||
|
const parts = [generateId("__")];
|
||
|
for (let i = 0; i < this.target.loopLevel; i++) {
|
||
|
parts.push(`\${key${i + 1}}`);
|
||
|
}
|
||
|
return `${currentKey} + \`${parts.join("__")}\``;
|
||
|
}
|
||
|
/**
|
||
|
* Formats a prop name and value into a string suitable to be inserted in the
|
||
|
* generated code. For example:
|
||
|
*
|
||
|
* Name Value Result
|
||
|
* ---------------------------------------------------------
|
||
|
* "number" "state" "number: ctx['state']"
|
||
|
* "something" "" "something: undefined"
|
||
|
* "some-prop" "state" "'some-prop': ctx['state']"
|
||
|
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||
|
*/
|
||
|
formatProp(name, value, attrsTranslationCtx, translationCtx) {
|
||
|
if (name.endsWith(".translate")) {
|
||
|
const attrTranslationCtx = (attrsTranslationCtx === null || attrsTranslationCtx === void 0 ? void 0 : attrsTranslationCtx[name]) || translationCtx;
|
||
|
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
|
||
|
}
|
||
|
else {
|
||
|
value = this.captureExpression(value);
|
||
|
}
|
||
|
if (name.includes(".")) {
|
||
|
let [_name, suffix] = name.split(".");
|
||
|
name = _name;
|
||
|
switch (suffix) {
|
||
|
case "bind":
|
||
|
value = `(${value}).bind(this)`;
|
||
|
break;
|
||
|
case "alike":
|
||
|
case "translate":
|
||
|
break;
|
||
|
default:
|
||
|
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||
|
}
|
||
|
}
|
||
|
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||
|
return `${name}: ${value || undefined}`;
|
||
|
}
|
||
|
formatPropObject(obj, attrsTranslationCtx, translationCtx) {
|
||
|
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));
|
||
|
}
|
||
|
getPropString(props, dynProps) {
|
||
|
let propString = `{${props.join(",")}}`;
|
||
|
if (dynProps) {
|
||
|
propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
|
||
|
}
|
||
|
return propString;
|
||
|
}
|
||
|
compileComponent(ast, ctx) {
|
||
|
let { block } = ctx;
|
||
|
// props
|
||
|
const hasSlotsProp = "slots" in (ast.props || {});
|
||
|
const props = ast.props
|
||
|
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
|
||
|
: [];
|
||
|
// slots
|
||
|
let slotDef = "";
|
||
|
if (ast.slots) {
|
||
|
let ctxStr = "ctx";
|
||
|
if (this.target.loopLevel || !this.hasSafeContext) {
|
||
|
ctxStr = generateId("ctx");
|
||
|
this.helpers.add("capture");
|
||
|
this.define(ctxStr, `capture(ctx)`);
|
||
|
}
|
||
|
let slotStr = [];
|
||
|
for (let slotName in ast.slots) {
|
||
|
const slotAst = ast.slots[slotName];
|
||
|
const params = [];
|
||
|
if (slotAst.content) {
|
||
|
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
|
||
|
params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`);
|
||
|
}
|
||
|
const scope = ast.slots[slotName].scope;
|
||
|
if (scope) {
|
||
|
params.push(`__scope: "${scope}"`);
|
||
|
}
|
||
|
if (ast.slots[slotName].attrs) {
|
||
|
params.push(...this.formatPropObject(ast.slots[slotName].attrs, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));
|
||
|
}
|
||
|
const slotInfo = `{${params.join(", ")}}`;
|
||
|
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||
|
}
|
||
|
slotDef = `{${slotStr.join(", ")}}`;
|
||
|
}
|
||
|
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
|
||
|
this.helpers.add("markRaw");
|
||
|
props.push(`slots: markRaw(${slotDef})`);
|
||
|
}
|
||
|
let propString = this.getPropString(props, ast.dynamicProps);
|
||
|
let propVar;
|
||
|
if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
|
||
|
propVar = generateId("props");
|
||
|
this.define(propVar, propString);
|
||
|
propString = propVar;
|
||
|
}
|
||
|
if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
|
||
|
this.helpers.add("markRaw");
|
||
|
this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
|
||
|
}
|
||
|
// cmap key
|
||
|
let expr;
|
||
|
if (ast.isDynamic) {
|
||
|
expr = generateId("Comp");
|
||
|
this.define(expr, compileExpr(ast.name));
|
||
|
}
|
||
|
else {
|
||
|
expr = `\`${ast.name}\``;
|
||
|
}
|
||
|
if (this.dev) {
|
||
|
this.addLine(`helpers.validateProps(${expr}, ${propVar}, this);`);
|
||
|
}
|
||
|
if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
|
||
|
// todo: check the forcenewblock condition
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
let keyArg = this.generateComponentKey();
|
||
|
if (ctx.tKeyExpr) {
|
||
|
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||
|
}
|
||
|
let id = generateId("comp");
|
||
|
const propList = [];
|
||
|
for (let p in ast.props || {}) {
|
||
|
let [name, suffix] = p.split(".");
|
||
|
if (!suffix) {
|
||
|
propList.push(`"${name}"`);
|
||
|
}
|
||
|
}
|
||
|
this.staticDefs.push({
|
||
|
id,
|
||
|
expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,
|
||
|
});
|
||
|
if (ast.isDynamic) {
|
||
|
// If the component class changes, this can cause delayed renders to go
|
||
|
// through if the key doesn't change. Use the component name for now.
|
||
|
// This means that two component classes with the same name isn't supported
|
||
|
// in t-component. We can generate a unique id per class later if needed.
|
||
|
keyArg = `(${expr}).name + ${keyArg}`;
|
||
|
}
|
||
|
let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
|
||
|
if (ast.isDynamic) {
|
||
|
blockExpr = `toggler(${expr}, ${blockExpr})`;
|
||
|
}
|
||
|
// event handling
|
||
|
if (ast.on) {
|
||
|
blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
|
||
|
}
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
this.insertBlock(blockExpr, block, ctx);
|
||
|
return block.varName;
|
||
|
}
|
||
|
wrapWithEventCatcher(expr, on) {
|
||
|
this.helpers.add("createCatcher");
|
||
|
let name = generateId("catcher");
|
||
|
let spec = {};
|
||
|
let handlers = [];
|
||
|
for (let ev in on) {
|
||
|
let handlerId = generateId("hdlr");
|
||
|
let idx = handlers.push(handlerId) - 1;
|
||
|
spec[ev] = idx;
|
||
|
const handler = this.generateHandlerCode(ev, on[ev]);
|
||
|
this.define(handlerId, handler);
|
||
|
}
|
||
|
this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
|
||
|
return `${name}(${expr}, [${handlers.join(",")}])`;
|
||
|
}
|
||
|
compileTSlot(ast, ctx) {
|
||
|
this.helpers.add("callSlot");
|
||
|
let { block } = ctx;
|
||
|
let blockString;
|
||
|
let slotName;
|
||
|
let dynamic = false;
|
||
|
let isMultiple = false;
|
||
|
if (ast.name.match(INTERP_REGEXP)) {
|
||
|
dynamic = true;
|
||
|
isMultiple = true;
|
||
|
slotName = interpolate(ast.name);
|
||
|
}
|
||
|
else {
|
||
|
slotName = "'" + ast.name + "'";
|
||
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||
|
this.slotNames.add(ast.name);
|
||
|
}
|
||
|
const attrs = { ...ast.attrs };
|
||
|
const dynProps = attrs["t-props"];
|
||
|
delete attrs["t-props"];
|
||
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||
|
if (isMultiple) {
|
||
|
key = this.generateComponentKey(key);
|
||
|
}
|
||
|
const props = ast.attrs
|
||
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||
|
: [];
|
||
|
const scope = this.getPropString(props, dynProps);
|
||
|
if (ast.defaultContent) {
|
||
|
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||
|
blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`;
|
||
|
}
|
||
|
else {
|
||
|
if (dynamic) {
|
||
|
let name = generateId("slot");
|
||
|
this.define(name, slotName);
|
||
|
blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;
|
||
|
}
|
||
|
else {
|
||
|
blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;
|
||
|
}
|
||
|
}
|
||
|
// event handling
|
||
|
if (ast.on) {
|
||
|
blockString = this.wrapWithEventCatcher(blockString, ast.on);
|
||
|
}
|
||
|
if (block) {
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
|
||
|
return block.varName;
|
||
|
}
|
||
|
compileTTranslation(ast, ctx) {
|
||
|
if (ast.content) {
|
||
|
return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
compileTTranslationContext(ast, ctx) {
|
||
|
if (ast.content) {
|
||
|
return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
compileTPortal(ast, ctx) {
|
||
|
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||
|
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||
|
}
|
||
|
let { block } = ctx;
|
||
|
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||
|
let ctxStr = "ctx";
|
||
|
if (this.target.loopLevel || !this.hasSafeContext) {
|
||
|
ctxStr = generateId("ctx");
|
||
|
this.helpers.add("capture");
|
||
|
this.define(ctxStr, `capture(ctx)`);
|
||
|
}
|
||
|
let id = generateId("comp");
|
||
|
this.staticDefs.push({
|
||
|
id,
|
||
|
expr: `app.createComponent(null, false, true, false, false)`,
|
||
|
});
|
||
|
const target = compileExpr(ast.target);
|
||
|
const key = this.generateComponentKey();
|
||
|
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||
|
if (block) {
|
||
|
this.insertAnchor(block);
|
||
|
}
|
||
|
block = this.createBlock(block, "multi", ctx);
|
||
|
this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
|
||
|
return block.varName;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Parser
|
||
|
// -----------------------------------------------------------------------------
|
||
|
const cache = new WeakMap();
|
||
|
function parse(xml, customDir) {
|
||
|
const ctx = {
|
||
|
inPreTag: false,
|
||
|
customDirectives: customDir,
|
||
|
};
|
||
|
if (typeof xml === "string") {
|
||
|
const elem = parseXML(`<t>${xml}</t>`).firstChild;
|
||
|
return _parse(elem, ctx);
|
||
|
}
|
||
|
let ast = cache.get(xml);
|
||
|
if (!ast) {
|
||
|
// we clone here the xml to prevent modifying it in place
|
||
|
ast = _parse(xml.cloneNode(true), ctx);
|
||
|
cache.set(xml, ast);
|
||
|
}
|
||
|
return ast;
|
||
|
}
|
||
|
function _parse(xml, ctx) {
|
||
|
normalizeXML(xml);
|
||
|
return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
|
||
|
}
|
||
|
function parseNode(node, ctx) {
|
||
|
if (!(node instanceof Element)) {
|
||
|
return parseTextCommentNode(node, ctx);
|
||
|
}
|
||
|
return (parseTCustom(node, ctx) ||
|
||
|
parseTDebugLog(node, ctx) ||
|
||
|
parseTForEach(node, ctx) ||
|
||
|
parseTIf(node, ctx) ||
|
||
|
parseTPortal(node, ctx) ||
|
||
|
parseTCall(node, ctx) ||
|
||
|
parseTCallBlock(node) ||
|
||
|
parseTEscNode(node, ctx) ||
|
||
|
parseTOutNode(node, ctx) ||
|
||
|
parseTKey(node, ctx) ||
|
||
|
parseTTranslation(node, ctx) ||
|
||
|
parseTTranslationContext(node, ctx) ||
|
||
|
parseTSlot(node, ctx) ||
|
||
|
parseComponent(node, ctx) ||
|
||
|
parseDOMNode(node, ctx) ||
|
||
|
parseTSetNode(node, ctx) ||
|
||
|
parseTNode(node, ctx));
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// <t /> tag
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTNode(node, ctx) {
|
||
|
if (node.tagName !== "t") {
|
||
|
return null;
|
||
|
}
|
||
|
return parseChildNodes(node, ctx);
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Text and Comment Nodes
|
||
|
// -----------------------------------------------------------------------------
|
||
|
const lineBreakRE = /[\r\n]/;
|
||
|
function parseTextCommentNode(node, ctx) {
|
||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||
|
let value = node.textContent || "";
|
||
|
if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
|
||
|
return null;
|
||
|
}
|
||
|
return { type: 0 /* Text */, value };
|
||
|
}
|
||
|
else if (node.nodeType === Node.COMMENT_NODE) {
|
||
|
return { type: 1 /* Comment */, value: node.textContent || "" };
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
function parseTCustom(node, ctx) {
|
||
|
if (!ctx.customDirectives) {
|
||
|
return null;
|
||
|
}
|
||
|
const nodeAttrsNames = node.getAttributeNames();
|
||
|
for (let attr of nodeAttrsNames) {
|
||
|
if (attr === "t-custom" || attr === "t-custom-") {
|
||
|
throw new OwlError("Missing custom directive name with t-custom directive");
|
||
|
}
|
||
|
if (attr.startsWith("t-custom-")) {
|
||
|
const directiveName = attr.split(".")[0].slice(9);
|
||
|
const customDirective = ctx.customDirectives[directiveName];
|
||
|
if (!customDirective) {
|
||
|
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
|
||
|
}
|
||
|
const value = node.getAttribute(attr);
|
||
|
const modifiers = attr.split(".").slice(1);
|
||
|
node.removeAttribute(attr);
|
||
|
try {
|
||
|
customDirective(node, value, modifiers);
|
||
|
}
|
||
|
catch (error) {
|
||
|
throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
|
||
|
}
|
||
|
return parseNode(node, ctx);
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// debugging
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTDebugLog(node, ctx) {
|
||
|
if (node.hasAttribute("t-debug")) {
|
||
|
node.removeAttribute("t-debug");
|
||
|
return {
|
||
|
type: 12 /* TDebug */,
|
||
|
content: parseNode(node, ctx),
|
||
|
};
|
||
|
}
|
||
|
if (node.hasAttribute("t-log")) {
|
||
|
const expr = node.getAttribute("t-log");
|
||
|
node.removeAttribute("t-log");
|
||
|
return {
|
||
|
type: 13 /* TLog */,
|
||
|
expr,
|
||
|
content: parseNode(node, ctx),
|
||
|
};
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Regular dom node
|
||
|
// -----------------------------------------------------------------------------
|
||
|
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
||
|
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
||
|
const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
|
||
|
function parseDOMNode(node, ctx) {
|
||
|
const { tagName } = node;
|
||
|
const dynamicTag = node.getAttribute("t-tag");
|
||
|
node.removeAttribute("t-tag");
|
||
|
if (tagName === "t" && !dynamicTag) {
|
||
|
return null;
|
||
|
}
|
||
|
if (tagName.startsWith("block-")) {
|
||
|
throw new OwlError(`Invalid tag name: '${tagName}'`);
|
||
|
}
|
||
|
ctx = Object.assign({}, ctx);
|
||
|
if (tagName === "pre") {
|
||
|
ctx.inPreTag = true;
|
||
|
}
|
||
|
let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
|
||
|
const ref = node.getAttribute("t-ref");
|
||
|
node.removeAttribute("t-ref");
|
||
|
const nodeAttrsNames = node.getAttributeNames();
|
||
|
let attrs = null;
|
||
|
let attrsTranslationCtx = null;
|
||
|
let on = null;
|
||
|
let model = null;
|
||
|
for (let attr of nodeAttrsNames) {
|
||
|
const value = node.getAttribute(attr);
|
||
|
if (attr === "t-on" || attr === "t-on-") {
|
||
|
throw new OwlError("Missing event name with t-on directive");
|
||
|
}
|
||
|
if (attr.startsWith("t-on-")) {
|
||
|
on = on || {};
|
||
|
on[attr.slice(5)] = value;
|
||
|
}
|
||
|
else if (attr.startsWith("t-model")) {
|
||
|
if (!["input", "select", "textarea"].includes(tagName)) {
|
||
|
throw new OwlError("The t-model directive only works with <input>, <textarea> and <select>");
|
||
|
}
|
||
|
let baseExpr, expr;
|
||
|
if (hasDotAtTheEnd.test(value)) {
|
||
|
const index = value.lastIndexOf(".");
|
||
|
baseExpr = value.slice(0, index);
|
||
|
expr = `'${value.slice(index + 1)}'`;
|
||
|
}
|
||
|
else if (hasBracketsAtTheEnd.test(value)) {
|
||
|
const index = value.lastIndexOf("[");
|
||
|
baseExpr = value.slice(0, index);
|
||
|
expr = value.slice(index + 1, -1);
|
||
|
}
|
||
|
else {
|
||
|
throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||
|
}
|
||
|
const typeAttr = node.getAttribute("type");
|
||
|
const isInput = tagName === "input";
|
||
|
const isSelect = tagName === "select";
|
||
|
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||
|
const isRadioInput = isInput && typeAttr === "radio";
|
||
|
const hasTrimMod = attr.includes(".trim");
|
||
|
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||
|
const hasNumberMod = attr.includes(".number");
|
||
|
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||
|
model = {
|
||
|
baseExpr,
|
||
|
expr,
|
||
|
targetAttr: isCheckboxInput ? "checked" : "value",
|
||
|
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||
|
eventType,
|
||
|
hasDynamicChildren: false,
|
||
|
shouldTrim: hasTrimMod,
|
||
|
shouldNumberize: hasNumberMod,
|
||
|
};
|
||
|
if (isSelect) {
|
||
|
// don't pollute the original ctx
|
||
|
ctx = Object.assign({}, ctx);
|
||
|
ctx.tModelInfo = model;
|
||
|
}
|
||
|
}
|
||
|
else if (attr.startsWith("block-")) {
|
||
|
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||
|
}
|
||
|
else if (attr === "xmlns") {
|
||
|
ns = value;
|
||
|
}
|
||
|
else if (attr.startsWith("t-translation-context-")) {
|
||
|
const attrName = attr.slice(22);
|
||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||
|
attrsTranslationCtx[attrName] = value;
|
||
|
}
|
||
|
else if (attr !== "t-name") {
|
||
|
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||
|
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||
|
}
|
||
|
const tModel = ctx.tModelInfo;
|
||
|
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
|
||
|
tModel.hasDynamicChildren = true;
|
||
|
}
|
||
|
attrs = attrs || {};
|
||
|
attrs[attr] = value;
|
||
|
}
|
||
|
}
|
||
|
if (ns) {
|
||
|
ctx.nameSpace = ns;
|
||
|
}
|
||
|
const children = parseChildren(node, ctx);
|
||
|
return {
|
||
|
type: 2 /* DomNode */,
|
||
|
tag: tagName,
|
||
|
dynamicTag,
|
||
|
attrs,
|
||
|
attrsTranslationCtx,
|
||
|
on,
|
||
|
ref,
|
||
|
content: children,
|
||
|
model,
|
||
|
ns,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-esc
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTEscNode(node, ctx) {
|
||
|
if (!node.hasAttribute("t-esc")) {
|
||
|
return null;
|
||
|
}
|
||
|
const escValue = node.getAttribute("t-esc");
|
||
|
node.removeAttribute("t-esc");
|
||
|
const tesc = {
|
||
|
type: 4 /* TEsc */,
|
||
|
expr: escValue,
|
||
|
defaultValue: node.textContent || "",
|
||
|
};
|
||
|
let ref = node.getAttribute("t-ref");
|
||
|
node.removeAttribute("t-ref");
|
||
|
const ast = parseNode(node, ctx);
|
||
|
if (!ast) {
|
||
|
return tesc;
|
||
|
}
|
||
|
if (ast.type === 2 /* DomNode */) {
|
||
|
return {
|
||
|
...ast,
|
||
|
ref,
|
||
|
content: [tesc],
|
||
|
};
|
||
|
}
|
||
|
return tesc;
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-out
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTOutNode(node, ctx) {
|
||
|
if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
|
||
|
return null;
|
||
|
}
|
||
|
if (node.hasAttribute("t-raw")) {
|
||
|
console.warn(`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`);
|
||
|
}
|
||
|
const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"));
|
||
|
node.removeAttribute("t-out");
|
||
|
node.removeAttribute("t-raw");
|
||
|
const tOut = { type: 8 /* TOut */, expr, body: null };
|
||
|
const ref = node.getAttribute("t-ref");
|
||
|
node.removeAttribute("t-ref");
|
||
|
const ast = parseNode(node, ctx);
|
||
|
if (!ast) {
|
||
|
return tOut;
|
||
|
}
|
||
|
if (ast.type === 2 /* DomNode */) {
|
||
|
tOut.body = ast.content.length ? ast.content : null;
|
||
|
return {
|
||
|
...ast,
|
||
|
ref,
|
||
|
content: [tOut],
|
||
|
};
|
||
|
}
|
||
|
return tOut;
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-foreach and t-key
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTForEach(node, ctx) {
|
||
|
if (!node.hasAttribute("t-foreach")) {
|
||
|
return null;
|
||
|
}
|
||
|
const html = node.outerHTML;
|
||
|
const collection = node.getAttribute("t-foreach");
|
||
|
node.removeAttribute("t-foreach");
|
||
|
const elem = node.getAttribute("t-as") || "";
|
||
|
node.removeAttribute("t-as");
|
||
|
const key = node.getAttribute("t-key");
|
||
|
if (!key) {
|
||
|
throw new OwlError(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
|
||
|
}
|
||
|
node.removeAttribute("t-key");
|
||
|
const memo = node.getAttribute("t-memo") || "";
|
||
|
node.removeAttribute("t-memo");
|
||
|
const body = parseNode(node, ctx);
|
||
|
if (!body) {
|
||
|
return null;
|
||
|
}
|
||
|
const hasNoTCall = !html.includes("t-call");
|
||
|
const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
|
||
|
const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
|
||
|
const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
|
||
|
const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
|
||
|
return {
|
||
|
type: 9 /* TForEach */,
|
||
|
collection,
|
||
|
elem,
|
||
|
body,
|
||
|
memo,
|
||
|
key,
|
||
|
hasNoFirst,
|
||
|
hasNoLast,
|
||
|
hasNoIndex,
|
||
|
hasNoValue,
|
||
|
};
|
||
|
}
|
||
|
function parseTKey(node, ctx) {
|
||
|
if (!node.hasAttribute("t-key")) {
|
||
|
return null;
|
||
|
}
|
||
|
const key = node.getAttribute("t-key");
|
||
|
node.removeAttribute("t-key");
|
||
|
const body = parseNode(node, ctx);
|
||
|
if (!body) {
|
||
|
return null;
|
||
|
}
|
||
|
return { type: 10 /* TKey */, expr: key, content: body };
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-call
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTCall(node, ctx) {
|
||
|
if (!node.hasAttribute("t-call")) {
|
||
|
return null;
|
||
|
}
|
||
|
const subTemplate = node.getAttribute("t-call");
|
||
|
const context = node.getAttribute("t-call-context");
|
||
|
node.removeAttribute("t-call");
|
||
|
node.removeAttribute("t-call-context");
|
||
|
if (node.tagName !== "t") {
|
||
|
const ast = parseNode(node, ctx);
|
||
|
const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };
|
||
|
if (ast && ast.type === 2 /* DomNode */) {
|
||
|
ast.content = [tcall];
|
||
|
return ast;
|
||
|
}
|
||
|
if (ast && ast.type === 11 /* TComponent */) {
|
||
|
return {
|
||
|
...ast,
|
||
|
slots: {
|
||
|
default: {
|
||
|
content: tcall,
|
||
|
scope: null,
|
||
|
on: null,
|
||
|
attrs: null,
|
||
|
attrsTranslationCtx: null,
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
const body = parseChildren(node, ctx);
|
||
|
return {
|
||
|
type: 7 /* TCall */,
|
||
|
name: subTemplate,
|
||
|
body: body.length ? body : null,
|
||
|
context,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-call-block
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTCallBlock(node, ctx) {
|
||
|
if (!node.hasAttribute("t-call-block")) {
|
||
|
return null;
|
||
|
}
|
||
|
const name = node.getAttribute("t-call-block");
|
||
|
return {
|
||
|
type: 15 /* TCallBlock */,
|
||
|
name,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-if
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTIf(node, ctx) {
|
||
|
if (!node.hasAttribute("t-if")) {
|
||
|
return null;
|
||
|
}
|
||
|
const condition = node.getAttribute("t-if");
|
||
|
node.removeAttribute("t-if");
|
||
|
const content = parseNode(node, ctx) || { type: 0 /* Text */, value: "" };
|
||
|
let nextElement = node.nextElementSibling;
|
||
|
// t-elifs
|
||
|
const tElifs = [];
|
||
|
while (nextElement && nextElement.hasAttribute("t-elif")) {
|
||
|
const condition = nextElement.getAttribute("t-elif");
|
||
|
nextElement.removeAttribute("t-elif");
|
||
|
const tElif = parseNode(nextElement, ctx);
|
||
|
const next = nextElement.nextElementSibling;
|
||
|
nextElement.remove();
|
||
|
nextElement = next;
|
||
|
if (tElif) {
|
||
|
tElifs.push({ condition, content: tElif });
|
||
|
}
|
||
|
}
|
||
|
// t-else
|
||
|
let tElse = null;
|
||
|
if (nextElement && nextElement.hasAttribute("t-else")) {
|
||
|
nextElement.removeAttribute("t-else");
|
||
|
tElse = parseNode(nextElement, ctx);
|
||
|
nextElement.remove();
|
||
|
}
|
||
|
return {
|
||
|
type: 5 /* TIf */,
|
||
|
condition,
|
||
|
content,
|
||
|
tElif: tElifs.length ? tElifs : null,
|
||
|
tElse,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// t-set directive
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTSetNode(node, ctx) {
|
||
|
if (!node.hasAttribute("t-set")) {
|
||
|
return null;
|
||
|
}
|
||
|
const name = node.getAttribute("t-set");
|
||
|
const value = node.getAttribute("t-value") || null;
|
||
|
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
|
||
|
let body = null;
|
||
|
if (node.textContent !== node.innerHTML) {
|
||
|
body = parseChildren(node, ctx);
|
||
|
}
|
||
|
return { type: 6 /* TSet */, name, value, defaultValue, body };
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Components
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Error messages when trying to use an unsupported directive on a component
|
||
|
const directiveErrorMap = new Map([
|
||
|
[
|
||
|
"t-ref",
|
||
|
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
|
||
|
],
|
||
|
["t-att", "t-att makes no sense on component: props are already treated as expressions"],
|
||
|
[
|
||
|
"t-attf",
|
||
|
"t-attf is not supported on components: use template strings for string interpolation in props",
|
||
|
],
|
||
|
]);
|
||
|
function parseComponent(node, ctx) {
|
||
|
let name = node.tagName;
|
||
|
const firstLetter = name[0];
|
||
|
let isDynamic = node.hasAttribute("t-component");
|
||
|
if (isDynamic && name !== "t") {
|
||
|
throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
|
||
|
}
|
||
|
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
||
|
return null;
|
||
|
}
|
||
|
if (isDynamic) {
|
||
|
name = node.getAttribute("t-component");
|
||
|
node.removeAttribute("t-component");
|
||
|
}
|
||
|
const dynamicProps = node.getAttribute("t-props");
|
||
|
node.removeAttribute("t-props");
|
||
|
const defaultSlotScope = node.getAttribute("t-slot-scope");
|
||
|
node.removeAttribute("t-slot-scope");
|
||
|
let on = null;
|
||
|
let props = null;
|
||
|
let propsTranslationCtx = null;
|
||
|
for (let name of node.getAttributeNames()) {
|
||
|
const value = node.getAttribute(name);
|
||
|
if (name.startsWith("t-translation-context-")) {
|
||
|
const attrName = name.slice(22);
|
||
|
propsTranslationCtx = propsTranslationCtx || {};
|
||
|
propsTranslationCtx[attrName] = value;
|
||
|
}
|
||
|
else if (name.startsWith("t-")) {
|
||
|
if (name.startsWith("t-on-")) {
|
||
|
on = on || {};
|
||
|
on[name.slice(5)] = value;
|
||
|
}
|
||
|
else {
|
||
|
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
|
||
|
throw new OwlError(message || `unsupported directive on Component: ${name}`);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
props = props || {};
|
||
|
props[name] = value;
|
||
|
}
|
||
|
}
|
||
|
let slots = null;
|
||
|
if (node.hasChildNodes()) {
|
||
|
const clone = node.cloneNode(true);
|
||
|
// named slots
|
||
|
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
||
|
for (let slotNode of slotNodes) {
|
||
|
if (slotNode.tagName !== "t") {
|
||
|
throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
|
||
|
}
|
||
|
const name = slotNode.getAttribute("t-set-slot");
|
||
|
// check if this is defined in a sub component (in which case it should
|
||
|
// be ignored)
|
||
|
let el = slotNode.parentElement;
|
||
|
let isInSubComponent = false;
|
||
|
while (el && el !== clone) {
|
||
|
if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
|
||
|
isInSubComponent = true;
|
||
|
break;
|
||
|
}
|
||
|
el = el.parentElement;
|
||
|
}
|
||
|
if (isInSubComponent || !el) {
|
||
|
continue;
|
||
|
}
|
||
|
slotNode.removeAttribute("t-set-slot");
|
||
|
slotNode.remove();
|
||
|
const slotAst = parseNode(slotNode, ctx);
|
||
|
let on = null;
|
||
|
let attrs = null;
|
||
|
let attrsTranslationCtx = null;
|
||
|
let scope = null;
|
||
|
for (let attributeName of slotNode.getAttributeNames()) {
|
||
|
const value = slotNode.getAttribute(attributeName);
|
||
|
if (attributeName === "t-slot-scope") {
|
||
|
scope = value;
|
||
|
continue;
|
||
|
}
|
||
|
else if (attributeName.startsWith("t-translation-context-")) {
|
||
|
const attrName = attributeName.slice(22);
|
||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||
|
attrsTranslationCtx[attrName] = value;
|
||
|
}
|
||
|
else if (attributeName.startsWith("t-on-")) {
|
||
|
on = on || {};
|
||
|
on[attributeName.slice(5)] = value;
|
||
|
}
|
||
|
else {
|
||
|
attrs = attrs || {};
|
||
|
attrs[attributeName] = value;
|
||
|
}
|
||
|
}
|
||
|
slots = slots || {};
|
||
|
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||
|
}
|
||
|
// default slot
|
||
|
const defaultContent = parseChildNodes(clone, ctx);
|
||
|
slots = slots || {};
|
||
|
// t-set-slot="default" has priority over content
|
||
|
if (defaultContent && !slots.default) {
|
||
|
slots.default = {
|
||
|
content: defaultContent,
|
||
|
on,
|
||
|
attrs: null,
|
||
|
attrsTranslationCtx: null,
|
||
|
scope: defaultSlotScope,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
type: 11 /* TComponent */,
|
||
|
name,
|
||
|
isDynamic,
|
||
|
dynamicProps,
|
||
|
props,
|
||
|
propsTranslationCtx,
|
||
|
slots,
|
||
|
on,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Slots
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTSlot(node, ctx) {
|
||
|
if (!node.hasAttribute("t-slot")) {
|
||
|
return null;
|
||
|
}
|
||
|
const name = node.getAttribute("t-slot");
|
||
|
node.removeAttribute("t-slot");
|
||
|
let attrs = null;
|
||
|
let attrsTranslationCtx = null;
|
||
|
let on = null;
|
||
|
for (let attributeName of node.getAttributeNames()) {
|
||
|
const value = node.getAttribute(attributeName);
|
||
|
if (attributeName.startsWith("t-on-")) {
|
||
|
on = on || {};
|
||
|
on[attributeName.slice(5)] = value;
|
||
|
}
|
||
|
else if (attributeName.startsWith("t-translation-context-")) {
|
||
|
const attrName = attributeName.slice(22);
|
||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||
|
attrsTranslationCtx[attrName] = value;
|
||
|
}
|
||
|
else {
|
||
|
attrs = attrs || {};
|
||
|
attrs[attributeName] = value;
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
type: 14 /* TSlot */,
|
||
|
name,
|
||
|
attrs,
|
||
|
attrsTranslationCtx,
|
||
|
on,
|
||
|
defaultContent: parseChildNodes(node, ctx),
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Translation
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTTranslation(node, ctx) {
|
||
|
if (node.getAttribute("t-translation") !== "off") {
|
||
|
return null;
|
||
|
}
|
||
|
node.removeAttribute("t-translation");
|
||
|
return {
|
||
|
type: 16 /* TTranslation */,
|
||
|
content: parseNode(node, ctx),
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Translation Context
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTTranslationContext(node, ctx) {
|
||
|
const translationCtx = node.getAttribute("t-translation-context");
|
||
|
if (!translationCtx) {
|
||
|
return null;
|
||
|
}
|
||
|
node.removeAttribute("t-translation-context");
|
||
|
return {
|
||
|
type: 17 /* TTranslationContext */,
|
||
|
content: parseNode(node, ctx),
|
||
|
translationCtx,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Portal
|
||
|
// -----------------------------------------------------------------------------
|
||
|
function parseTPortal(node, ctx) {
|
||
|
if (!node.hasAttribute("t-portal")) {
|
||
|
return null;
|
||
|
}
|
||
|
const target = node.getAttribute("t-portal");
|
||
|
node.removeAttribute("t-portal");
|
||
|
const content = parseNode(node, ctx);
|
||
|
if (!content) {
|
||
|
return {
|
||
|
type: 0 /* Text */,
|
||
|
value: "",
|
||
|
};
|
||
|
}
|
||
|
return {
|
||
|
type: 18 /* TPortal */,
|
||
|
target,
|
||
|
content,
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// helpers
|
||
|
// -----------------------------------------------------------------------------
|
||
|
/**
|
||
|
* Parse all the child nodes of a given node and return a list of ast elements
|
||
|
*/
|
||
|
function parseChildren(node, ctx) {
|
||
|
const children = [];
|
||
|
for (let child of node.childNodes) {
|
||
|
const childAst = parseNode(child, ctx);
|
||
|
if (childAst) {
|
||
|
if (childAst.type === 3 /* Multi */) {
|
||
|
children.push(...childAst.content);
|
||
|
}
|
||
|
else {
|
||
|
children.push(childAst);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return children;
|
||
|
}
|
||
|
/**
|
||
|
* Parse all the child nodes of a given node and return an ast if possible.
|
||
|
* In the case there are multiple children, they are wrapped in a astmulti.
|
||
|
*/
|
||
|
function parseChildNodes(node, ctx) {
|
||
|
const children = parseChildren(node, ctx);
|
||
|
switch (children.length) {
|
||
|
case 0:
|
||
|
return null;
|
||
|
case 1:
|
||
|
return children[0];
|
||
|
default:
|
||
|
return { type: 3 /* Multi */, content: children };
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Normalizes the content of an Element so that t-if/t-elif/t-else directives
|
||
|
* immediately follow one another (by removing empty text nodes or comments).
|
||
|
* Throws an error when a conditional branching statement is malformed. This
|
||
|
* function modifies the Element in place.
|
||
|
*
|
||
|
* @param el the element containing the tree that should be normalized
|
||
|
*/
|
||
|
function normalizeTIf(el) {
|
||
|
let tbranch = el.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 = (name) => prevElem.getAttribute(name);
|
||
|
let nattr = (name) => +!!node.getAttribute(name);
|
||
|
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||
|
if (pattr("t-foreach")) {
|
||
|
throw new OwlError("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 OwlError("Only one conditional branching directive is allowed per node");
|
||
|
}
|
||
|
// All text (with only spaces) and comment nodes (nodeType 8) between
|
||
|
// branch nodes are removed
|
||
|
let textNode;
|
||
|
while ((textNode = node.previousSibling) !== prevElem) {
|
||
|
if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
|
||
|
throw new OwlError("text is not allowed between branching directives");
|
||
|
}
|
||
|
textNode.remove();
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
throw new OwlError("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Normalizes the content of an Element so that t-esc directives on components
|
||
|
* are removed and instead places a <t t-esc=""> as the default slot of the
|
||
|
* component. Also throws if the component already has content. This function
|
||
|
* modifies the Element in place.
|
||
|
*
|
||
|
* @param el the element containing the tree that should be normalized
|
||
|
*/
|
||
|
function normalizeTEscTOut(el) {
|
||
|
for (const d of ["t-esc", "t-out"]) {
|
||
|
const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
|
||
|
for (const el of elements) {
|
||
|
if (el.childNodes.length) {
|
||
|
throw new OwlError(`Cannot have ${d} on a component that already has content`);
|
||
|
}
|
||
|
const value = el.getAttribute(d);
|
||
|
el.removeAttribute(d);
|
||
|
const t = el.ownerDocument.createElement("t");
|
||
|
if (value != null) {
|
||
|
t.setAttribute(d, value);
|
||
|
}
|
||
|
el.appendChild(t);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Normalizes the tree inside a given element and do some preliminary validation
|
||
|
* on it. This function modifies the Element in place.
|
||
|
*
|
||
|
* @param el the element containing the tree that should be normalized
|
||
|
*/
|
||
|
function normalizeXML(el) {
|
||
|
normalizeTIf(el);
|
||
|
normalizeTEscTOut(el);
|
||
|
}
|
||
|
|
||
|
function compile(template, options = {
|
||
|
hasGlobalValues: false,
|
||
|
}) {
|
||
|
// parsing
|
||
|
const ast = parse(template, options.customDirectives);
|
||
|
// some work
|
||
|
const hasSafeContext = template instanceof Node
|
||
|
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
|
||
|
: !template.includes("t-set") && !template.includes("t-call");
|
||
|
// code generation
|
||
|
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
||
|
const code = codeGenerator.generateCode();
|
||
|
// template function
|
||
|
try {
|
||
|
return new Function("app, bdom, helpers", code);
|
||
|
}
|
||
|
catch (originalError) {
|
||
|
const { name } = options;
|
||
|
const nameStr = name ? `template "${name}"` : "anonymous template";
|
||
|
const err = new OwlError(`Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}`);
|
||
|
err.cause = originalError;
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// do not modify manually. This file is generated by the release script.
|
||
|
const version = "2.6.1";
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Scheduler
|
||
|
// -----------------------------------------------------------------------------
|
||
|
class Scheduler {
|
||
|
constructor() {
|
||
|
this.tasks = new Set();
|
||
|
this.frame = 0;
|
||
|
this.delayedRenders = [];
|
||
|
this.cancelledNodes = new Set();
|
||
|
this.processing = false;
|
||
|
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||
|
}
|
||
|
addFiber(fiber) {
|
||
|
this.tasks.add(fiber.root);
|
||
|
}
|
||
|
scheduleDestroy(node) {
|
||
|
this.cancelledNodes.add(node);
|
||
|
if (this.frame === 0) {
|
||
|
this.frame = this.requestAnimationFrame(() => this.processTasks());
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Process all current tasks. This only applies to the fibers that are ready.
|
||
|
* Other tasks are left unchanged.
|
||
|
*/
|
||
|
flush() {
|
||
|
if (this.delayedRenders.length) {
|
||
|
let renders = this.delayedRenders;
|
||
|
this.delayedRenders = [];
|
||
|
for (let f of renders) {
|
||
|
if (f.root && f.node.status !== 3 /* DESTROYED */ && f.node.fiber === f) {
|
||
|
f.render();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (this.frame === 0) {
|
||
|
this.frame = this.requestAnimationFrame(() => this.processTasks());
|
||
|
}
|
||
|
}
|
||
|
processTasks() {
|
||
|
if (this.processing) {
|
||
|
return;
|
||
|
}
|
||
|
this.processing = true;
|
||
|
this.frame = 0;
|
||
|
for (let node of this.cancelledNodes) {
|
||
|
node._destroy();
|
||
|
}
|
||
|
this.cancelledNodes.clear();
|
||
|
for (let task of this.tasks) {
|
||
|
this.processFiber(task);
|
||
|
}
|
||
|
for (let task of this.tasks) {
|
||
|
if (task.node.status === 3 /* DESTROYED */) {
|
||
|
this.tasks.delete(task);
|
||
|
}
|
||
|
}
|
||
|
this.processing = false;
|
||
|
}
|
||
|
processFiber(fiber) {
|
||
|
if (fiber.root !== fiber) {
|
||
|
this.tasks.delete(fiber);
|
||
|
return;
|
||
|
}
|
||
|
const hasError = fibersInError.has(fiber);
|
||
|
if (hasError && fiber.counter !== 0) {
|
||
|
this.tasks.delete(fiber);
|
||
|
return;
|
||
|
}
|
||
|
if (fiber.node.status === 3 /* DESTROYED */) {
|
||
|
this.tasks.delete(fiber);
|
||
|
return;
|
||
|
}
|
||
|
if (fiber.counter === 0) {
|
||
|
if (!hasError) {
|
||
|
fiber.complete();
|
||
|
}
|
||
|
// at this point, the fiber should have been applied to the DOM, so we can
|
||
|
// remove it from the task list. If it is not the case, it means that there
|
||
|
// was an error and an error handler triggered a new rendering that recycled
|
||
|
// the fiber, so in that case, we actually want to keep the fiber around,
|
||
|
// otherwise it will just be ignored.
|
||
|
if (fiber.appliedToDom) {
|
||
|
this.tasks.delete(fiber);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// capture the value of requestAnimationFrame as soon as possible, to avoid
|
||
|
// interactions with other code, such as test frameworks that override them
|
||
|
Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
||
|
|
||
|
let hasBeenLogged = false;
|
||
|
const apps = new Set();
|
||
|
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
|
||
|
class App extends TemplateSet {
|
||
|
constructor(Root, config = {}) {
|
||
|
super(config);
|
||
|
this.scheduler = new Scheduler();
|
||
|
this.subRoots = new Set();
|
||
|
this.root = null;
|
||
|
this.name = config.name || "";
|
||
|
this.Root = Root;
|
||
|
apps.add(this);
|
||
|
if (config.test) {
|
||
|
this.dev = true;
|
||
|
}
|
||
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||
|
console.info(`Owl is running in 'dev' mode.`);
|
||
|
hasBeenLogged = true;
|
||
|
}
|
||
|
const env = config.env || {};
|
||
|
const descrs = Object.getOwnPropertyDescriptors(env);
|
||
|
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
|
||
|
this.props = config.props || {};
|
||
|
}
|
||
|
mount(target, options) {
|
||
|
const root = this.createRoot(this.Root, { props: this.props });
|
||
|
this.root = root.node;
|
||
|
this.subRoots.delete(root.node);
|
||
|
return root.mount(target, options);
|
||
|
}
|
||
|
createRoot(Root, config = {}) {
|
||
|
const props = config.props || {};
|
||
|
// hack to make sure the sub root get the sub env if necessary. for owl 3,
|
||
|
// would be nice to rethink the initialization process to make sure that
|
||
|
// we can create a ComponentNode and give it explicitely the env, instead
|
||
|
// of looking it up in the app
|
||
|
const env = this.env;
|
||
|
if (config.env) {
|
||
|
this.env = config.env;
|
||
|
}
|
||
|
const restore = saveCurrent();
|
||
|
const node = this.makeNode(Root, props);
|
||
|
restore();
|
||
|
if (config.env) {
|
||
|
this.env = env;
|
||
|
}
|
||
|
this.subRoots.add(node);
|
||
|
return {
|
||
|
node,
|
||
|
mount: (target, options) => {
|
||
|
App.validateTarget(target);
|
||
|
if (this.dev) {
|
||
|
validateProps(Root, props, { __owl__: { app: this } });
|
||
|
}
|
||
|
const prom = this.mountNode(node, target, options);
|
||
|
return prom;
|
||
|
},
|
||
|
destroy: () => {
|
||
|
this.subRoots.delete(node);
|
||
|
node.destroy();
|
||
|
this.scheduler.processTasks();
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
makeNode(Component, props) {
|
||
|
return new ComponentNode(Component, props, this, null, null);
|
||
|
}
|
||
|
mountNode(node, target, options) {
|
||
|
const promise = new Promise((resolve, reject) => {
|
||
|
let isResolved = false;
|
||
|
// manually set a onMounted callback.
|
||
|
// that way, we are independant from the current node.
|
||
|
node.mounted.push(() => {
|
||
|
resolve(node.component);
|
||
|
isResolved = true;
|
||
|
});
|
||
|
// Manually add the last resort error handler on the node
|
||
|
let handlers = nodeErrorHandlers.get(node);
|
||
|
if (!handlers) {
|
||
|
handlers = [];
|
||
|
nodeErrorHandlers.set(node, handlers);
|
||
|
}
|
||
|
handlers.unshift((e) => {
|
||
|
if (!isResolved) {
|
||
|
reject(e);
|
||
|
}
|
||
|
throw e;
|
||
|
});
|
||
|
});
|
||
|
node.mountComponent(target, options);
|
||
|
return promise;
|
||
|
}
|
||
|
destroy() {
|
||
|
if (this.root) {
|
||
|
for (let subroot of this.subRoots) {
|
||
|
subroot.destroy();
|
||
|
}
|
||
|
this.root.destroy();
|
||
|
this.scheduler.processTasks();
|
||
|
}
|
||
|
apps.delete(this);
|
||
|
}
|
||
|
createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, propList) {
|
||
|
const isDynamic = !isStatic;
|
||
|
let arePropsDifferent;
|
||
|
const hasNoProp = propList.length === 0;
|
||
|
if (hasSlotsProp) {
|
||
|
arePropsDifferent = (_1, _2) => true;
|
||
|
}
|
||
|
else if (hasDynamicPropList) {
|
||
|
arePropsDifferent = function (props1, props2) {
|
||
|
for (let k in props1) {
|
||
|
if (props1[k] !== props2[k]) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return Object.keys(props1).length !== Object.keys(props2).length;
|
||
|
};
|
||
|
}
|
||
|
else if (hasNoProp) {
|
||
|
arePropsDifferent = (_1, _2) => false;
|
||
|
}
|
||
|
else {
|
||
|
arePropsDifferent = function (props1, props2) {
|
||
|
for (let p of propList) {
|
||
|
if (props1[p] !== props2[p]) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
};
|
||
|
}
|
||
|
const updateAndRender = ComponentNode.prototype.updateAndRender;
|
||
|
const initiateRender = ComponentNode.prototype.initiateRender;
|
||
|
return (props, key, ctx, parent, C) => {
|
||
|
let children = ctx.children;
|
||
|
let node = children[key];
|
||
|
if (isDynamic && node && node.component.constructor !== C) {
|
||
|
node = undefined;
|
||
|
}
|
||
|
const parentFiber = ctx.fiber;
|
||
|
if (node) {
|
||
|
if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
|
||
|
node.forceNextRender = false;
|
||
|
updateAndRender.call(node, props, parentFiber);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
// new component
|
||
|
if (isStatic) {
|
||
|
const components = parent.constructor.components;
|
||
|
if (!components) {
|
||
|
throw new OwlError(`Cannot find the definition of component "${name}", missing static components key in parent`);
|
||
|
}
|
||
|
C = components[name];
|
||
|
if (!C) {
|
||
|
throw new OwlError(`Cannot find the definition of component "${name}"`);
|
||
|
}
|
||
|
else if (!(C.prototype instanceof Component)) {
|
||
|
throw new OwlError(`"${name}" is not a Component. It must inherit from the Component class`);
|
||
|
}
|
||
|
}
|
||
|
node = new ComponentNode(C, props, this, ctx, key);
|
||
|
children[key] = node;
|
||
|
initiateRender.call(node, new Fiber(node, parentFiber));
|
||
|
}
|
||
|
parentFiber.childrenMap[key] = node;
|
||
|
return node;
|
||
|
};
|
||
|
}
|
||
|
handleError(...args) {
|
||
|
return handleError(...args);
|
||
|
}
|
||
|
}
|
||
|
App.validateTarget = validateTarget;
|
||
|
App.apps = apps;
|
||
|
App.version = version;
|
||
|
async function mount(C, target, config = {}) {
|
||
|
return new App(C, config).mount(target, config);
|
||
|
}
|
||
|
|
||
|
const mainEventHandler = (data, ev, currentTarget) => {
|
||
|
const { data: _data, modifiers } = filterOutModifiersFromData(data);
|
||
|
data = _data;
|
||
|
let stopped = false;
|
||
|
if (modifiers.length) {
|
||
|
let selfMode = false;
|
||
|
const isSelf = ev.target === currentTarget;
|
||
|
for (const mod of modifiers) {
|
||
|
switch (mod) {
|
||
|
case "self":
|
||
|
selfMode = true;
|
||
|
if (isSelf) {
|
||
|
continue;
|
||
|
}
|
||
|
else {
|
||
|
return stopped;
|
||
|
}
|
||
|
case "prevent":
|
||
|
if ((selfMode && isSelf) || !selfMode)
|
||
|
ev.preventDefault();
|
||
|
continue;
|
||
|
case "stop":
|
||
|
if ((selfMode && isSelf) || !selfMode)
|
||
|
ev.stopPropagation();
|
||
|
stopped = true;
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
|
||
|
// We check this rather than data[0] being truthy (or typeof function) so that it crashes
|
||
|
// as expected when there is a handler expression that evaluates to a falsy value
|
||
|
if (Object.hasOwnProperty.call(data, 0)) {
|
||
|
const handler = data[0];
|
||
|
if (typeof handler !== "function") {
|
||
|
throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
|
||
|
}
|
||
|
let node = data[1] ? data[1].__owl__ : null;
|
||
|
if (node ? node.status === 1 /* MOUNTED */ : true) {
|
||
|
handler.call(node ? node.component : null, ev);
|
||
|
}
|
||
|
}
|
||
|
return stopped;
|
||
|
};
|
||
|
|
||
|
function status(component) {
|
||
|
switch (component.__owl__.status) {
|
||
|
case 0 /* NEW */:
|
||
|
return "new";
|
||
|
case 2 /* CANCELLED */:
|
||
|
return "cancelled";
|
||
|
case 1 /* MOUNTED */:
|
||
|
return "mounted";
|
||
|
case 3 /* DESTROYED */:
|
||
|
return "destroyed";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// useRef
|
||
|
// -----------------------------------------------------------------------------
|
||
|
/**
|
||
|
* The purpose of this hook is to allow components to get a reference to a sub
|
||
|
* html node or component.
|
||
|
*/
|
||
|
function useRef(name) {
|
||
|
const node = getCurrent();
|
||
|
const refs = node.refs;
|
||
|
return {
|
||
|
get el() {
|
||
|
const el = refs[name];
|
||
|
return inOwnerDocument(el) ? el : null;
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// useEnv and useSubEnv
|
||
|
// -----------------------------------------------------------------------------
|
||
|
/**
|
||
|
* This hook is useful as a building block for some customized hooks, that may
|
||
|
* need a reference to the env of the component calling them.
|
||
|
*/
|
||
|
function useEnv() {
|
||
|
return getCurrent().component.env;
|
||
|
}
|
||
|
function extendEnv(currentEnv, extension) {
|
||
|
const env = Object.create(currentEnv);
|
||
|
const descrs = Object.getOwnPropertyDescriptors(extension);
|
||
|
return Object.freeze(Object.defineProperties(env, descrs));
|
||
|
}
|
||
|
/**
|
||
|
* 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(envExtension) {
|
||
|
const node = getCurrent();
|
||
|
node.component.env = extendEnv(node.component.env, envExtension);
|
||
|
useChildSubEnv(envExtension);
|
||
|
}
|
||
|
function useChildSubEnv(envExtension) {
|
||
|
const node = getCurrent();
|
||
|
node.childEnv = extendEnv(node.childEnv, envExtension);
|
||
|
}
|
||
|
/**
|
||
|
* This hook will run a callback when a component is mounted and patched, and
|
||
|
* will run a cleanup function before patching and before unmounting the
|
||
|
* the component.
|
||
|
*
|
||
|
* @template T
|
||
|
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||
|
* @param {()=>[...T]} [computeDependencies=()=>[NaN]] a callback to compute
|
||
|
* dependencies that will decide if the effect needs to be cleaned up and
|
||
|
* run again. If the dependencies did not change, the effect will not run
|
||
|
* again. The default value returns an array containing only NaN because
|
||
|
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||
|
*/
|
||
|
function useEffect(effect, computeDependencies = () => [NaN]) {
|
||
|
let cleanup;
|
||
|
let dependencies;
|
||
|
onMounted(() => {
|
||
|
dependencies = computeDependencies();
|
||
|
cleanup = effect(...dependencies);
|
||
|
});
|
||
|
onPatched(() => {
|
||
|
const newDeps = computeDependencies();
|
||
|
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
|
||
|
if (shouldReapply) {
|
||
|
dependencies = newDeps;
|
||
|
if (cleanup) {
|
||
|
cleanup();
|
||
|
}
|
||
|
cleanup = effect(...dependencies);
|
||
|
}
|
||
|
});
|
||
|
onWillUnmount(() => cleanup && cleanup());
|
||
|
}
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// useExternalListener
|
||
|
// -----------------------------------------------------------------------------
|
||
|
/**
|
||
|
* When a component needs to listen to DOM Events on element(s) that are not
|
||
|
* part of his hierarchy, we can use the `useExternalListener` hook.
|
||
|
* It will correctly add and remove the event listener, whenever the
|
||
|
* component is mounted and unmounted.
|
||
|
*
|
||
|
* Example:
|
||
|
* a menu needs to listen to the click on window to be closed automatically
|
||
|
*
|
||
|
* Usage:
|
||
|
* in the constructor of the OWL component that needs to be notified,
|
||
|
* `useExternalListener(window, 'click', this._doSomething);`
|
||
|
* */
|
||
|
function useExternalListener(target, eventName, handler, eventParams) {
|
||
|
const node = getCurrent();
|
||
|
const boundHandler = handler.bind(node.component);
|
||
|
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||
|
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||
|
}
|
||
|
|
||
|
config.shouldNormalizeDom = false;
|
||
|
config.mainEventHandler = mainEventHandler;
|
||
|
const blockDom = {
|
||
|
config,
|
||
|
// bdom entry points
|
||
|
mount: mount$1,
|
||
|
patch,
|
||
|
remove,
|
||
|
// bdom block types
|
||
|
list,
|
||
|
multi,
|
||
|
text,
|
||
|
toggler,
|
||
|
createBlock,
|
||
|
html,
|
||
|
comment,
|
||
|
};
|
||
|
const __info__ = {
|
||
|
version: App.version,
|
||
|
};
|
||
|
|
||
|
TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {
|
||
|
return compile(template, {
|
||
|
name,
|
||
|
dev: this.dev,
|
||
|
translateFn: this.translateFn,
|
||
|
translatableAttributes: this.translatableAttributes,
|
||
|
customDirectives: this.customDirectives,
|
||
|
hasGlobalValues: this.hasGlobalValues,
|
||
|
});
|
||
|
};
|
||
|
|
||
|
exports.App = App;
|
||
|
exports.Component = Component;
|
||
|
exports.EventBus = EventBus;
|
||
|
exports.OwlError = OwlError;
|
||
|
exports.__info__ = __info__;
|
||
|
exports.batched = batched;
|
||
|
exports.blockDom = blockDom;
|
||
|
exports.loadFile = loadFile;
|
||
|
exports.markRaw = markRaw;
|
||
|
exports.markup = markup;
|
||
|
exports.mount = mount;
|
||
|
exports.onError = onError;
|
||
|
exports.onMounted = onMounted;
|
||
|
exports.onPatched = onPatched;
|
||
|
exports.onRendered = onRendered;
|
||
|
exports.onWillDestroy = onWillDestroy;
|
||
|
exports.onWillPatch = onWillPatch;
|
||
|
exports.onWillRender = onWillRender;
|
||
|
exports.onWillStart = onWillStart;
|
||
|
exports.onWillUnmount = onWillUnmount;
|
||
|
exports.onWillUpdateProps = onWillUpdateProps;
|
||
|
exports.reactive = reactive;
|
||
|
exports.status = status;
|
||
|
exports.toRaw = toRaw;
|
||
|
exports.useChildSubEnv = useChildSubEnv;
|
||
|
exports.useComponent = useComponent;
|
||
|
exports.useEffect = useEffect;
|
||
|
exports.useEnv = useEnv;
|
||
|
exports.useExternalListener = useExternalListener;
|
||
|
exports.useRef = useRef;
|
||
|
exports.useState = useState;
|
||
|
exports.useSubEnv = useSubEnv;
|
||
|
exports.validate = validate;
|
||
|
exports.validateType = validateType;
|
||
|
exports.whenReady = whenReady;
|
||
|
exports.xml = xml;
|
||
|
|
||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
|
||
|
|
||
|
__info__.date = '2025-03-05T08:37:58.580Z';
|
||
|
__info__.hash = '2b5cea9';
|
||
|
__info__.url = 'https://github.com/odoo/owl';
|
||
|
|
||
|
|
||
|
})(this.owl = this.owl || {});
|