mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
0931a4dc5b
Before this commit, the handlers were bound to the current context, which is often the component but not necessarily. In some cases, a sub scope can be created (with t-foreach, or slots, or t-call), and the context is actually an object with the actual component instance it its prototype chain, but not the component.
312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
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();"
|
|
};
|
|
|
|
interface HandlerInfo {
|
|
event: string;
|
|
handler: string;
|
|
}
|
|
|
|
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
|
|
|
|
export function makeHandlerCode(
|
|
ctx,
|
|
fullName,
|
|
value,
|
|
putInCache: boolean,
|
|
modcodes = MODS_CODE
|
|
): HandlerInfo {
|
|
const [event, ...mods] = fullName.slice(5).split(".");
|
|
if (!event) {
|
|
throw new Error("Missing event name with t-on directive");
|
|
}
|
|
let code: string;
|
|
// check if it is a method with no args, a method with args or an expression
|
|
let args: string = "";
|
|
const name: string = value.replace(/\(.*\)/, function(_args) {
|
|
args = _args.slice(1, -1);
|
|
return "";
|
|
});
|
|
const isMethodCall = name.match(FNAMEREGEXP);
|
|
|
|
// then generate code
|
|
if (isMethodCall) {
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
const comp = `utils.getComponent(context)`;
|
|
if (args) {
|
|
let argId = ctx.generateID();
|
|
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
|
|
code = `${comp}['${name}'](...args${argId}, e);`;
|
|
putInCache = false;
|
|
} else {
|
|
code = `${comp}['${name}'](e);`;
|
|
}
|
|
} else {
|
|
// if we get here, then it is an expression
|
|
putInCache = false;
|
|
code = ctx.formatExpression(value);
|
|
}
|
|
const modCode = mods.map(mod => modcodes[mod]).join("");
|
|
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
|
|
if (putInCache) {
|
|
const key = ctx.generateTemplateKey(event);
|
|
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
|
|
handler = `extra.handlers[${key}]`;
|
|
}
|
|
return { event, handler };
|
|
}
|
|
|
|
QWeb.addDirective({
|
|
name: "on",
|
|
priority: 90,
|
|
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
|
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
|
|
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
|
|
}
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-ref
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "ref",
|
|
priority: 95,
|
|
atNodeCreation({ ctx, value, addNodeHook }) {
|
|
ctx.rootContext.shouldDefineRefs = true;
|
|
const refKey = `ref${ctx.generateID()}`;
|
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
|
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
|
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
|
|
}
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// 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");
|
|
elm.classList.remove(name + "-leave-active");
|
|
elm.classList.remove(name + "-leave-to");
|
|
const finalize = () => {
|
|
elm.classList.remove(name + "-enter-active");
|
|
elm.classList.remove(name + "-enter-to");
|
|
};
|
|
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 = () => {
|
|
if (!elm.classList.contains(name + "-leave-active")) {
|
|
return;
|
|
}
|
|
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-slot
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "slot",
|
|
priority: 80,
|
|
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
|
const slotKey = ctx.generateID();
|
|
ctx.addLine(
|
|
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
|
);
|
|
ctx.addIf(`slot${slotKey}`);
|
|
let parentNode = `c${ctx.parentNode}`;
|
|
if (!ctx.parentNode) {
|
|
ctx.rootContext.shouldDefineResult = true;
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
parentNode = `children${ctx.generateID()}`;
|
|
ctx.addLine(`let ${parentNode}= []`);
|
|
ctx.addLine(`result = {}`);
|
|
}
|
|
ctx.addLine(
|
|
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
|
|
);
|
|
if (!ctx.parentNode) {
|
|
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
|
|
}
|
|
if (node.hasChildNodes()) {
|
|
ctx.addElse();
|
|
const nodeCopy = <Element>node.cloneNode(true);
|
|
nodeCopy.removeAttribute("t-slot");
|
|
qweb._compileNode(nodeCopy, ctx);
|
|
}
|
|
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, addNodeHook }) {
|
|
const type = node.getAttribute("type");
|
|
let handler;
|
|
let event = fullName.includes(".lazy") ? "change" : "input";
|
|
|
|
// we keep here a reference to the "base expression" (if the expression
|
|
// is `t-model="some.expr.value", then the base expression is "some.expr").
|
|
// This is necessary so we can capture it in the handler closure.
|
|
let expr = ctx.formatExpression(value);
|
|
const index = expr.lastIndexOf(".");
|
|
const baseExpr = expr.slice(0, index);
|
|
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
|
|
|
|
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
|
|
const key = ctx.generateTemplateKey();
|
|
if (node.tagName === "select") {
|
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
|
addNodeHook("create", `n.elm.value=${expr};`);
|
|
event = "change";
|
|
handler = `(ev) => {${expr} = ev.target.value}`;
|
|
} else if (type === "checkbox") {
|
|
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
|
|
handler = `(ev) => {${expr} = ev.target.checked}`;
|
|
} else if (type === "radio") {
|
|
const nodeValue = node.getAttribute("value")!;
|
|
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
|
|
handler = `(ev) => {${expr} = ev.target.value}`;
|
|
event = "click";
|
|
} else {
|
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
|
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
|
let valueCode = `ev.target.value${trimCode}`;
|
|
if (fullName.includes(".number")) {
|
|
ctx.rootContext.shouldDefineUtils = true;
|
|
valueCode = `utils.toNumber(${valueCode})`;
|
|
}
|
|
handler = `(ev) => {${expr} = ${valueCode}}`;
|
|
}
|
|
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
|
|
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
|
|
}
|
|
});
|
|
|
|
//------------------------------------------------------------------------------
|
|
// t-key
|
|
//------------------------------------------------------------------------------
|
|
QWeb.addDirective({
|
|
name: "key",
|
|
priority: 45,
|
|
atNodeEncounter({ ctx, value }) {
|
|
let id = ctx.generateID();
|
|
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
|
|
ctx.currentKey = `nodeKey${id}`;
|
|
}
|
|
});
|