mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[REF] owl: reorganize src code in sub files
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Observer } from "./observer";
|
||||
import { QWeb, CompiledTemplate, UTILS } from "./qweb_core";
|
||||
import { h, patch, VNode } from "./vdom";
|
||||
import { Observer } from "../core/observer";
|
||||
import { CompiledTemplate, QWeb } from "../qweb/index";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import "./directive";
|
||||
import "./props_validation";
|
||||
|
||||
/**
|
||||
* Owl Component System
|
||||
@@ -135,7 +137,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
if (QWeb.dev) {
|
||||
// we only validate props for root widgets here. "Regular" widget
|
||||
// props are validated by the t-component directive
|
||||
UTILS.validateProps(this.constructor, this.props);
|
||||
QWeb.utils.validateProps(this.constructor, this.props);
|
||||
}
|
||||
this.env.qweb.on("update", this, () => {
|
||||
if (this.__owl__.isMounted) {
|
||||
@@ -1,186 +1,5 @@
|
||||
import { QWeb, UTILS } from "./qweb_core";
|
||||
import { VNode } from "./vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-component/t-keepalive
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
* - t-model
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();"
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
ctx.addIf(`!context['${handlerName}']`);
|
||||
ctx.addLine(
|
||||
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)`
|
||||
);
|
||||
ctx.closeIf();
|
||||
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
||||
let handler;
|
||||
if (mods.length > 0) {
|
||||
handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
||||
} else {
|
||||
handler = `context['${handlerName}'].bind(${params})`;
|
||||
}
|
||||
if (extraArgs) {
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
UTILS.nextFrame = function(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
UTILS.transitionInsert = function(vn: VNode, name: string) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
// remove potential duplicated vnode that is currently being removed, to
|
||||
// prevent from having twice the same node in the DOM during an animation
|
||||
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||
if (dup) {
|
||||
dup.remove();
|
||||
}
|
||||
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
UTILS.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
elm.setAttribute("data-owl-key", vn.key!);
|
||||
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
elm.addEventListener("transitionend", cb, { once: true });
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ value, addNodeHook }) {
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `this.utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { MODS_CODE } from "../qweb/extensions";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-component
|
||||
@@ -190,7 +9,7 @@ const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
|
||||
self: "if (e.target !== vn.elm) {return}"
|
||||
});
|
||||
|
||||
UTILS.defineProxy = function defineProxy(target, source) {
|
||||
QWeb.utils.defineProxy = function defineProxy(target, source) {
|
||||
for (let k in source) {
|
||||
Object.defineProperty(target, k, {
|
||||
get() {
|
||||
@@ -673,198 +492,3 @@ QWeb.addDirective({
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
UTILS.validateProps = function(Widget, props: Object) {
|
||||
const propsDef = (<any>Widget).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
const propName = propsDef[i];
|
||||
if (propName[propName.length - 1] === "?") {
|
||||
// optional prop
|
||||
break;
|
||||
}
|
||||
if (!props[propName]) {
|
||||
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
||||
}
|
||||
}
|
||||
for (let key in props) {
|
||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
if (!isValid) {
|
||||
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (propDef === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
let result = isValidProp(prop, propDef.type);
|
||||
if (propDef.type === Array) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-mounted
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "mounted",
|
||||
priority: 97,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const eventName = fullName.slice(5);
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handler = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
}
|
||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "slot",
|
||||
priority: 80,
|
||||
atNodeEncounter({ ctx, value }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
ctx.addLine(
|
||||
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
|
||||
ctx.parentNode
|
||||
}, vars: extra.vars, parent: owner}));`
|
||||
);
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-model
|
||||
//------------------------------------------------------------------------------
|
||||
UTILS.toNumber = function(val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "model",
|
||||
priority: 42,
|
||||
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||
event = "change";
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||
} else if (type === "checkbox") {
|
||||
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
|
||||
} else if (type === "radio") {
|
||||
const nodeValue = node.getAttribute("value")!;
|
||||
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||
event = "click";
|
||||
} else {
|
||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||
let valueCode = `ev.target.value${trimCode}`;
|
||||
if (fullName.includes(".number")) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
valueCode = `utils.toNumber(${valueCode})`;
|
||||
}
|
||||
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
|
||||
QWeb.utils.validateProps = function(Widget, props: Object) {
|
||||
const propsDef = (<any>Widget).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
const propName = propsDef[i];
|
||||
if (propName[propName.length - 1] === "?") {
|
||||
// optional prop
|
||||
break;
|
||||
}
|
||||
if (!props[propName]) {
|
||||
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
||||
}
|
||||
}
|
||||
for (let key in props) {
|
||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
if (!isValid) {
|
||||
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (propDef === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
let result = isValidProp(prop, propDef.type);
|
||||
if (propDef.type === Array) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+11
-16
@@ -4,23 +4,19 @@
|
||||
*
|
||||
* Note that dynamic values, such as a date or a commit hash are added by rollup
|
||||
*/
|
||||
export { Component } from "./component";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
|
||||
export const misc = {EventBus, Observer};
|
||||
// we need to import manually the extra directives so they can register
|
||||
// themselves in QWeb, otherwise these files will not even be loaded.
|
||||
import "./qweb_directives";
|
||||
import "./qweb_extensions";
|
||||
import { QWeb } from "./qweb_core";
|
||||
export { QWeb };
|
||||
|
||||
import { Store, ConnectedComponent } from "./store";
|
||||
|
||||
export const store = {Store, ConnectedComponent};
|
||||
import { EventBus } from "./core/event_bus";
|
||||
import { Observer } from "./core/observer";
|
||||
import { QWeb } from "./qweb/index";
|
||||
import { ConnectedComponent } from "./store/connected_component";
|
||||
import { Store } from "./store/store";
|
||||
import * as _utils from "./utils";
|
||||
|
||||
export { Component } from "./component/component";
|
||||
export { QWeb };
|
||||
export const core = { EventBus, Observer };
|
||||
export const store = { Store, ConnectedComponent };
|
||||
export const utils = _utils;
|
||||
|
||||
export const __info__ = {};
|
||||
|
||||
Object.defineProperty(__info__, "mode", {
|
||||
@@ -39,4 +35,3 @@ Object.defineProperty(__info__, "mode", {
|
||||
}
|
||||
}
|
||||
});
|
||||
export const utils = _utils;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context, QWeb, UTILS } from "./qweb_core";
|
||||
import { QWebExprVar } from "./qweb_expressions";
|
||||
import { Context } from "./context";
|
||||
import { QWebExprVar } from "./expression_parser";
|
||||
import { QWeb } from "./qweb";
|
||||
|
||||
/**
|
||||
* Owl QWeb Directives
|
||||
@@ -18,7 +19,7 @@ import { QWebExprVar } from "./qweb_expressions";
|
||||
//------------------------------------------------------------------------------
|
||||
// t-esc and t-raw
|
||||
//------------------------------------------------------------------------------
|
||||
(<any>UTILS).getFragment = function(str: string): DocumentFragment {
|
||||
QWeb.utils.getFragment = function(str: string): DocumentFragment {
|
||||
const temp = document.createElement("template");
|
||||
temp.innerHTML = str;
|
||||
return temp.content;
|
||||
@@ -57,7 +58,8 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
}
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
|
||||
let tempNodeID = ctx.generateID();
|
||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
|
||||
@@ -255,11 +257,11 @@ QWeb.addDirective({
|
||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
||||
ctx.indent();
|
||||
ctx.addToScope(name + '_first', 'i === 0');
|
||||
ctx.addToScope(name + '_last', `i === _length${keysID} - 1`);
|
||||
ctx.addToScope(name + '_index', 'i');
|
||||
ctx.addToScope(name + "_first", "i === 0");
|
||||
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
|
||||
ctx.addToScope(name + "_index", "i");
|
||||
ctx.addToScope(name, `_${keysID}[i]`);
|
||||
ctx.addToScope(name + '_value', `_${valuesID}[i]`);
|
||||
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
||||
if (!shouldWarn && node.tagName === "t") {
|
||||
@@ -0,0 +1,167 @@
|
||||
import { compileExpr, QWebVar } from "./expression_parser";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Context {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: QWebVar } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null = null;
|
||||
parentTextNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: Context;
|
||||
caller: Element | undefined;
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineResult: boolean = false;
|
||||
shouldProtectContext: boolean = false;
|
||||
shouldTrackScope: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
allowMultipleRoots: boolean = false;
|
||||
hasParentWidget: boolean = false;
|
||||
scopeVars: any[] = [];
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("var h = this.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
const id = this.rootContext.nextID++;
|
||||
return id;
|
||||
}
|
||||
|
||||
generateCode(): string[] {
|
||||
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
|
||||
if (shouldTrackScope) {
|
||||
// add some vars to scope if needed
|
||||
for (let scopeVar of this.scopeVars.reverse()) {
|
||||
let { index, key, indent } = scopeVar;
|
||||
const prefix = new Array(indent + 2).join(" ");
|
||||
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
|
||||
}
|
||||
this.code.unshift(" const scope = Object.create(null);");
|
||||
}
|
||||
if (this.shouldProtectContext) {
|
||||
this.code.unshift(" context = Object.create(context);");
|
||||
}
|
||||
if (this.shouldDefineResult) {
|
||||
this.code.unshift(" let result;");
|
||||
}
|
||||
if (this.shouldDefineOwner) {
|
||||
// this is necessary to prevent some directives (t-forach for ex) to
|
||||
// pollute the rendering context by adding some keys in it.
|
||||
this.code.unshift(" let owner = context;");
|
||||
}
|
||||
if (this.shouldDefineParent) {
|
||||
if (this.hasParentWidget) {
|
||||
this.code.unshift(" let parent = extra.parent;");
|
||||
} else {
|
||||
this.code.unshift(" let parent = context;");
|
||||
}
|
||||
}
|
||||
if (this.shouldDefineQWeb) {
|
||||
this.code.unshift(" let QWeb = this.constructor;");
|
||||
}
|
||||
if (this.shouldDefineUtils) {
|
||||
this.code.unshift(" let utils = this.constructor.utils;");
|
||||
}
|
||||
return this.code;
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
if (
|
||||
!this.allowMultipleRoots &&
|
||||
this === this.rootContext &&
|
||||
(this.parentNode || this.parentTextNode)
|
||||
) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
subContext(key: keyof Context, value: any): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string): number {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
return this.code.length - 1;
|
||||
}
|
||||
|
||||
addToScope(key: string, expr: string) {
|
||||
const index = this.addLine(`context.${key} = ${expr};`);
|
||||
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an expression for being consumed at render time. Its main job
|
||||
* is to
|
||||
* - replace unknown variables by a lookup in the context
|
||||
* - replace already defined variables by their internal name
|
||||
*/
|
||||
formatExpression(expr: string): string {
|
||||
return compileExpr(expr, this.variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform string interpolation on the given string. Note that if the whole
|
||||
* string is an expression, it simply returns it (formatted and enclosed in
|
||||
* parentheses).
|
||||
* For instance:
|
||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(/\{\{.*?\}\}/g);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { VNode } from "../vdom/index";
|
||||
import { QWeb } from "./qweb";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
* - t-model
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
export const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();"
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
ctx.addIf(`!context['${handlerName}']`);
|
||||
ctx.addLine(
|
||||
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)`
|
||||
);
|
||||
ctx.closeIf();
|
||||
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
||||
let handler;
|
||||
if (mods.length > 0) {
|
||||
handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
||||
} else {
|
||||
handler = `context['${handlerName}'].bind(${params})`;
|
||||
}
|
||||
if (extraArgs) {
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.nextFrame = function(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
// remove potential duplicated vnode that is currently being removed, to
|
||||
// prevent from having twice the same node in the DOM during an animation
|
||||
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||
if (dup) {
|
||||
dup.remove();
|
||||
}
|
||||
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
elm.setAttribute("data-owl-key", vn.key!);
|
||||
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
elm.addEventListener("transitionend", cb, { once: true });
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `utils.transitionRemove(vn, '${name}', rm);`
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-mounted
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "mounted",
|
||||
priority: 97,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const eventName = fullName.slice(5);
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handler = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
}
|
||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "slot",
|
||||
priority: 80,
|
||||
atNodeEncounter({ ctx, value }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
ctx.addLine(
|
||||
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
|
||||
ctx.parentNode
|
||||
}, vars: extra.vars, parent: owner}));`
|
||||
);
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-model
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.toNumber = function(val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "model",
|
||||
priority: 42,
|
||||
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||
event = "change";
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||
} else if (type === "checkbox") {
|
||||
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
|
||||
} else if (type === "radio") {
|
||||
const nodeValue = node.getAttribute("value")!;
|
||||
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||
event = "click";
|
||||
} else {
|
||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||
let valueCode = `ev.target.value${trimCode}`;
|
||||
if (fullName.includes(".number")) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
valueCode = `utils.toNumber(${valueCode})`;
|
||||
}
|
||||
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import "./base_directives";
|
||||
import "./extensions";
|
||||
|
||||
export { CompiledTemplate, QWeb } from "./qweb";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VNode, h, patch } from "./vdom";
|
||||
import { QWebVar, compileExpr } from "./qweb_expressions";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { EventBus } from "../core/event_bus";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import { Context } from "./context";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
@@ -80,14 +80,12 @@ const NODE_HOOKS_PARAMS = {
|
||||
};
|
||||
|
||||
interface Utils {
|
||||
h: typeof h;
|
||||
toObj(expr: any): Object;
|
||||
shallowEqual(p1: Object, p2: Object): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const UTILS: Utils = {
|
||||
h: h,
|
||||
const UTILS: Utils = {
|
||||
toObj(expr) {
|
||||
if (typeof expr === "string") {
|
||||
expr = expr.trim();
|
||||
@@ -149,9 +147,10 @@ let nextID = 1;
|
||||
//------------------------------------------------------------------------------
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils = UTILS;
|
||||
static utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
|
||||
h = h;
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
|
||||
@@ -571,7 +570,8 @@ export class QWeb extends EventBus {
|
||||
let formattedValue = v.id || ctx.formatExpression(v);
|
||||
|
||||
if (attName === "class") {
|
||||
formattedValue = `this.utils.toObj(${formattedValue})`;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
formattedValue = `utils.toObj(${formattedValue})`;
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
|
||||
} else {
|
||||
@@ -673,169 +673,3 @@ export class QWeb extends EventBus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Context {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: QWebVar } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null = null;
|
||||
parentTextNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: Context;
|
||||
caller: Element | undefined;
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineResult: boolean = false;
|
||||
shouldProtectContext: boolean = false;
|
||||
shouldTrackScope: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
allowMultipleRoots: boolean = false;
|
||||
hasParentWidget: boolean = false;
|
||||
scopeVars: any[] = [];
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("var h = this.utils.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
const id = this.rootContext.nextID++;
|
||||
return id;
|
||||
}
|
||||
|
||||
generateCode(): string[] {
|
||||
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
|
||||
if (shouldTrackScope) {
|
||||
// add some vars to scope if needed
|
||||
for (let scopeVar of this.scopeVars.reverse()) {
|
||||
let { index, key, indent } = scopeVar;
|
||||
const prefix = new Array(indent + 2).join(" ");
|
||||
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
|
||||
}
|
||||
this.code.unshift(" const scope = Object.create(null);");
|
||||
}
|
||||
if (this.shouldProtectContext) {
|
||||
this.code.unshift(" context = Object.create(context);");
|
||||
}
|
||||
if (this.shouldDefineResult) {
|
||||
this.code.unshift(" let result;");
|
||||
}
|
||||
if (this.shouldDefineOwner) {
|
||||
// this is necessary to prevent some directives (t-forach for ex) to
|
||||
// pollute the rendering context by adding some keys in it.
|
||||
this.code.unshift(" let owner = context;");
|
||||
}
|
||||
if (this.shouldDefineParent) {
|
||||
if (this.hasParentWidget) {
|
||||
this.code.unshift(" let parent = extra.parent;");
|
||||
} else {
|
||||
this.code.unshift(" let parent = context;");
|
||||
}
|
||||
}
|
||||
if (this.shouldDefineQWeb) {
|
||||
this.code.unshift(" let QWeb = this.constructor;");
|
||||
}
|
||||
if (this.shouldDefineUtils) {
|
||||
this.code.unshift(" let utils = this.utils;");
|
||||
}
|
||||
return this.code;
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
if (
|
||||
!this.allowMultipleRoots &&
|
||||
this === this.rootContext &&
|
||||
(this.parentNode || this.parentTextNode)
|
||||
) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
subContext(key: keyof Context, value: any): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string): number {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
return this.code.length - 1;
|
||||
}
|
||||
|
||||
addToScope(key: string, expr: string) {
|
||||
const index = this.addLine(`context.${key} = ${expr};`);
|
||||
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an expression for being consumed at render time. Its main job
|
||||
* is to
|
||||
* - replace unknown variables by a lookup in the context
|
||||
* - replace already defined variables by their internal name
|
||||
*/
|
||||
formatExpression(expr: string): string {
|
||||
return compileExpr(expr, this.variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform string interpolation on the given string. Note that if the whole
|
||||
* string is an expression, it simply returns it (formatted and enclosed in
|
||||
* parentheses).
|
||||
* For instance:
|
||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(/\{\{.*?\}\}/g);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Component, Env } from "../component/component";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Connect function
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function revNumber<T extends Object>(o: T): number {
|
||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
||||
return (<any>o).__owl__.rev;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function deepRevNumber<T extends Object>(o: T): number {
|
||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
||||
return (<any>o).__owl__.deepRev;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type HashFunction = (a: any, b: any) => number;
|
||||
|
||||
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
|
||||
deep: boolean = true;
|
||||
getStore(env) {
|
||||
return env.store;
|
||||
}
|
||||
|
||||
hashFunction: HashFunction = ({ storeProps }, options) => {
|
||||
let refFunction = this.deep ? deepRevNumber : revNumber;
|
||||
if ("__owl__" in storeProps) {
|
||||
return refFunction(storeProps);
|
||||
}
|
||||
const { currentStoreProps } = options;
|
||||
let hash = 0;
|
||||
for (let key in storeProps) {
|
||||
const val = storeProps[key];
|
||||
const hashVal = refFunction(val);
|
||||
if (hashVal === 0) {
|
||||
if (val !== currentStoreProps[key]) {
|
||||
options.didChange = true;
|
||||
}
|
||||
} else {
|
||||
hash += hashVal;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
static mapStoreToProps(storeState, ownProps, getters) {
|
||||
return {};
|
||||
}
|
||||
constructor(parent, props?: any) {
|
||||
super(parent, props);
|
||||
const store = this.getStore(this.env);
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||
store.state,
|
||||
ownProps,
|
||||
store.getters
|
||||
);
|
||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
||||
this.props = mergedProps;
|
||||
(<any>this.__owl__).ownProps = ownProps;
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
(<any>this.__owl__).store = store;
|
||||
(<any>this.__owl__).storeHash = this.hashFunction(
|
||||
{
|
||||
state: store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
{
|
||||
currentStoreProps: storeProps
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* We do not use the mounted hook here for a subtle reason: we want the
|
||||
* updates to be called for the parents before the children. However,
|
||||
* if we use the mounted hook, this will be done in the reverse order.
|
||||
*/
|
||||
__callMounted() {
|
||||
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
|
||||
super.__callMounted();
|
||||
}
|
||||
willUnmount() {
|
||||
(<any>this.__owl__).store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
|
||||
async __checkUpdate(updateId) {
|
||||
if (updateId === (<any>this.__owl__).currentUpdateId) {
|
||||
return;
|
||||
}
|
||||
const ownProps = (<any>this.__owl__).ownProps;
|
||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||
(<any>this.__owl__).store.state,
|
||||
ownProps,
|
||||
(<any>this.__owl__).store.getters
|
||||
);
|
||||
const options: any = {
|
||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||
};
|
||||
const storeHash = this.hashFunction(
|
||||
{
|
||||
state: (<any>this.__owl__).store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
options
|
||||
);
|
||||
let didChange = options.didChange;
|
||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
||||
didChange = true;
|
||||
(<any>this.__owl__).storeHash = storeHash;
|
||||
}
|
||||
if (didChange) {
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
await this.__updateProps(ownProps, false);
|
||||
}
|
||||
}
|
||||
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
||||
const __owl__ = <any>this.__owl__;
|
||||
__owl__.currentUpdateId = __owl__.store._updateId;
|
||||
if (__owl__.ownProps !== nextProps) {
|
||||
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
|
||||
__owl__.store.state,
|
||||
nextProps,
|
||||
__owl__.store.getters
|
||||
);
|
||||
}
|
||||
__owl__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
|
||||
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Env } from "./component";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
import { Env } from "../component/component";
|
||||
import { EventBus } from "../core/event_bus";
|
||||
import { Observer } from "../core/observer";
|
||||
|
||||
/**
|
||||
* Owl Store
|
||||
@@ -166,141 +166,3 @@ export class Store extends EventBus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Connect function
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function revNumber<T extends Object>(o: T): number {
|
||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
||||
return (<any>o).__owl__.rev;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function deepRevNumber<T extends Object>(o: T): number {
|
||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
||||
return (<any>o).__owl__.deepRev;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type HashFunction = (a: any, b: any) => number;
|
||||
|
||||
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
|
||||
deep: boolean = true;
|
||||
getStore(env) {
|
||||
return env.store;
|
||||
}
|
||||
|
||||
hashFunction: HashFunction = ({ storeProps }, options) => {
|
||||
let refFunction = this.deep ? deepRevNumber : revNumber;
|
||||
if ("__owl__" in storeProps) {
|
||||
return refFunction(storeProps);
|
||||
}
|
||||
const { currentStoreProps } = options;
|
||||
let hash = 0;
|
||||
for (let key in storeProps) {
|
||||
const val = storeProps[key];
|
||||
const hashVal = refFunction(val);
|
||||
if (hashVal === 0) {
|
||||
if (val !== currentStoreProps[key]) {
|
||||
options.didChange = true;
|
||||
}
|
||||
} else {
|
||||
hash += hashVal;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
static mapStoreToProps(storeState, ownProps, getters) {
|
||||
return {};
|
||||
}
|
||||
constructor(parent, props?: any) {
|
||||
super(parent, props);
|
||||
const store = this.getStore(this.env);
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||
store.state,
|
||||
ownProps,
|
||||
store.getters
|
||||
);
|
||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
||||
this.props = mergedProps;
|
||||
(<any>this.__owl__).ownProps = ownProps;
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
(<any>this.__owl__).store = store;
|
||||
(<any>this.__owl__).storeHash = this.hashFunction(
|
||||
{
|
||||
state: store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
{
|
||||
currentStoreProps: storeProps
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* We do not use the mounted hook here for a subtle reason: we want the
|
||||
* updates to be called for the parents before the children. However,
|
||||
* if we use the mounted hook, this will be done in the reverse order.
|
||||
*/
|
||||
__callMounted() {
|
||||
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
|
||||
super.__callMounted();
|
||||
}
|
||||
willUnmount() {
|
||||
(<any>this.__owl__).store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
|
||||
async __checkUpdate(updateId) {
|
||||
if (updateId === (<any>this.__owl__).currentUpdateId) {
|
||||
return;
|
||||
}
|
||||
const ownProps = (<any>this.__owl__).ownProps;
|
||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||
(<any>this.__owl__).store.state,
|
||||
ownProps,
|
||||
(<any>this.__owl__).store.getters
|
||||
);
|
||||
const options: any = {
|
||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||
};
|
||||
const storeHash = this.hashFunction(
|
||||
{
|
||||
state: (<any>this.__owl__).store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
options
|
||||
);
|
||||
let didChange = options.didChange;
|
||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
||||
didChange = true;
|
||||
(<any>this.__owl__).storeHash = storeHash;
|
||||
}
|
||||
if (didChange) {
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
await this.__updateProps(ownProps, false);
|
||||
}
|
||||
}
|
||||
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
||||
const __owl__ = <any>this.__owl__;
|
||||
__owl__.currentUpdateId = __owl__.store._updateId;
|
||||
if (__owl__.ownProps !== nextProps) {
|
||||
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
|
||||
__owl__.store.state,
|
||||
nextProps,
|
||||
__owl__.store.getters
|
||||
);
|
||||
}
|
||||
__owl__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
|
||||
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules";
|
||||
import { init } from "./vdom";
|
||||
//------------------------------------------------------------------------------
|
||||
// patch
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export { h, VNode } from "./vdom";
|
||||
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Module, VNode, VNodeData } from "./vdom";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/props.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
cur: any,
|
||||
old: any,
|
||||
elm = vnode.elm,
|
||||
oldProps = (oldVnode.data as VNodeData).props,
|
||||
props = (vnode.data as VNodeData).props;
|
||||
|
||||
if (!oldProps && !props) return;
|
||||
if (oldProps === props) return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
|
||||
for (key in oldProps) {
|
||||
if (!props[key]) {
|
||||
delete (elm as any)[key];
|
||||
}
|
||||
}
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule = {
|
||||
create: updateProps,
|
||||
update: updateProps
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/eventlisteners.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
if (typeof handler === "function") {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode);
|
||||
} else if (typeof handler === "object") {
|
||||
// call handler with arguments
|
||||
if (typeof handler[0] === "function") {
|
||||
// special case for single argument for performance
|
||||
if (handler.length === 2) {
|
||||
handler[0].call(vnode, handler[1], event, vnode);
|
||||
} else {
|
||||
var args = handler.slice(1);
|
||||
args.push(event);
|
||||
args.push(vnode);
|
||||
handler[0].apply(vnode, args);
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: Event, vnode: VNode) {
|
||||
var name = event.type,
|
||||
on = (vnode.data as VNodeData).on;
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on && on[name]) {
|
||||
invokeHandler(on[name], vnode, event);
|
||||
}
|
||||
}
|
||||
|
||||
function createListener() {
|
||||
return function handler(event: Event) {
|
||||
handleEvent(event, (handler as any).vnode);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
var oldOn = (oldVnode.data as VNodeData).on,
|
||||
oldListener = (oldVnode as any).listener,
|
||||
oldElm: Element = oldVnode.elm as Element,
|
||||
on = vnode && (vnode.data as VNodeData).on,
|
||||
elm: Element = (vnode && vnode.elm) as Element,
|
||||
name: string;
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// attributes.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
|
||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
elm: Element = vnode.elm as Element,
|
||||
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
||||
attrs = (vnode.data as VNodeData).attrs;
|
||||
|
||||
if (!oldAttrs && !attrs) return;
|
||||
if (oldAttrs === attrs) return;
|
||||
oldAttrs = oldAttrs || {};
|
||||
attrs = attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, "");
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur);
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur);
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur);
|
||||
} else {
|
||||
elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attrsModule = {
|
||||
create: updateAttrs,
|
||||
update: updateAttrs
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// class.ts
|
||||
//------------------------------------------------------------------------------
|
||||
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||
var cur: any,
|
||||
name: string,
|
||||
elm: Element,
|
||||
oldClass = (oldVnode.data as VNodeData).class,
|
||||
klass = (vnode.data as VNodeData).class;
|
||||
|
||||
if (!oldClass && !klass) return;
|
||||
if (oldClass === klass) return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
|
||||
elm = vnode.elm as Element;
|
||||
|
||||
for (name in oldClass) {
|
||||
if (!klass[name]) {
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const classModule = { create: updateClass, update: updateClass } as Module;
|
||||
+16
-256
@@ -27,6 +27,21 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
type Props = Record<string, any>;
|
||||
type Attrs = Record<string, string | number | boolean>;
|
||||
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
|
||||
[event: string]: EventListener;
|
||||
};
|
||||
|
||||
export interface Module {
|
||||
pre: PreHook;
|
||||
create: CreateHook;
|
||||
update: UpdateHook;
|
||||
destroy: DestroyHook;
|
||||
remove: RemoveHook;
|
||||
post: PostHook;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// vnode.ts
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -41,7 +56,7 @@ export interface VNode {
|
||||
key: Key | undefined;
|
||||
}
|
||||
|
||||
interface VNodeData {
|
||||
export interface VNodeData {
|
||||
props?: Props;
|
||||
attrs?: Attrs;
|
||||
on?: On;
|
||||
@@ -631,258 +646,3 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
||||
}
|
||||
return vnode(sel, data, children, text, undefined);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/props.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type Props = Record<string, any>;
|
||||
|
||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
cur: any,
|
||||
old: any,
|
||||
elm = vnode.elm,
|
||||
oldProps = (oldVnode.data as VNodeData).props,
|
||||
props = (vnode.data as VNodeData).props;
|
||||
|
||||
if (!oldProps && !props) return;
|
||||
if (oldProps === props) return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
|
||||
for (key in oldProps) {
|
||||
if (!props[key]) {
|
||||
delete (elm as any)[key];
|
||||
}
|
||||
}
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule = {
|
||||
create: updateProps,
|
||||
update: updateProps
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/module.ts
|
||||
//------------------------------------------------------------------------------
|
||||
interface Module {
|
||||
pre: PreHook;
|
||||
create: CreateHook;
|
||||
update: UpdateHook;
|
||||
destroy: DestroyHook;
|
||||
remove: RemoveHook;
|
||||
post: PostHook;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/eventlisteners.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
|
||||
[event: string]: EventListener;
|
||||
};
|
||||
|
||||
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
if (typeof handler === "function") {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode);
|
||||
} else if (typeof handler === "object") {
|
||||
// call handler with arguments
|
||||
if (typeof handler[0] === "function") {
|
||||
// special case for single argument for performance
|
||||
if (handler.length === 2) {
|
||||
handler[0].call(vnode, handler[1], event, vnode);
|
||||
} else {
|
||||
var args = handler.slice(1);
|
||||
args.push(event);
|
||||
args.push(vnode);
|
||||
handler[0].apply(vnode, args);
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: Event, vnode: VNode) {
|
||||
var name = event.type,
|
||||
on = (vnode.data as VNodeData).on;
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on && on[name]) {
|
||||
invokeHandler(on[name], vnode, event);
|
||||
}
|
||||
}
|
||||
|
||||
function createListener() {
|
||||
return function handler(event: Event) {
|
||||
handleEvent(event, (handler as any).vnode);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
var oldOn = (oldVnode.data as VNodeData).on,
|
||||
oldListener = (oldVnode as any).listener,
|
||||
oldElm: Element = oldVnode.elm as Element,
|
||||
on = vnode && (vnode.data as VNodeData).on,
|
||||
elm: Element = (vnode && vnode.elm) as Element,
|
||||
name: string;
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// attributes.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type Attrs = Record<string, string | number | boolean>;
|
||||
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
|
||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
elm: Element = vnode.elm as Element,
|
||||
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
||||
attrs = (vnode.data as VNodeData).attrs;
|
||||
|
||||
if (!oldAttrs && !attrs) return;
|
||||
if (oldAttrs === attrs) return;
|
||||
oldAttrs = oldAttrs || {};
|
||||
attrs = attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, "");
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur);
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur);
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur);
|
||||
} else {
|
||||
elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attrsModule = {
|
||||
create: updateAttrs,
|
||||
update: updateAttrs
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// class.ts
|
||||
//------------------------------------------------------------------------------
|
||||
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||
var cur: any,
|
||||
name: string,
|
||||
elm: Element,
|
||||
oldClass = (oldVnode.data as VNodeData).class,
|
||||
klass = (vnode.data as VNodeData).class;
|
||||
|
||||
if (!oldClass && !klass) return;
|
||||
if (oldClass === klass) return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
|
||||
elm = vnode.elm as Element;
|
||||
|
||||
for (name in oldClass) {
|
||||
if (!klass[name]) {
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const classModule = { create: updateClass, update: updateClass } as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// patch
|
||||
//------------------------------------------------------------------------------
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
|
||||
Reference in New Issue
Block a user