mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
f8073cb153
Before this commit, owl would incorrectly skip patching html properties when the new value is the same as the value used in the previous render. However, this is incorrect, since the user may have changed the value by clicking on a checkbox, or changing some text in an input. So, to be more correct, owl has to force the update whenever it encounters a prop. This is however hard to do without impacting the main update loop, so we kind of work around the problem by using String and Boolean instance, instead of primitive values.
173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
import type { Setter } from "./block_compiler";
|
|
|
|
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(this: HTMLElement, key: string, value: any) {
|
|
switch (value) {
|
|
case false:
|
|
case undefined:
|
|
removeAttribute.call(this, key);
|
|
break;
|
|
case true:
|
|
elemSetAttribute.call(this, key, "");
|
|
break;
|
|
default:
|
|
elemSetAttribute.call(this, key, value);
|
|
}
|
|
}
|
|
|
|
export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
|
return function (this: HTMLElement, value: any) {
|
|
setAttribute.call(this, attr, value);
|
|
};
|
|
}
|
|
|
|
export function attrsSetter(this: HTMLElement, attrs: any) {
|
|
if (isArray(attrs)) {
|
|
setAttribute.call(this, attrs[0], attrs[1]);
|
|
} else {
|
|
for (let k in attrs) {
|
|
setAttribute.call(this, k, attrs[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
|
if (isArray(attrs)) {
|
|
const name = attrs[0];
|
|
const val = attrs[1];
|
|
if (name === oldAttrs[0]) {
|
|
if (val === oldAttrs[1]) {
|
|
return;
|
|
}
|
|
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)) {
|
|
removeAttribute.call(this, k);
|
|
}
|
|
}
|
|
for (let k in attrs) {
|
|
const val = attrs[k];
|
|
if (val !== oldAttrs[k]) {
|
|
setAttribute.call(this, k, val);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function toClassObj(expr: string | number | { [c: string]: any }) {
|
|
const result: { [c: string]: any } = {};
|
|
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 as any) {
|
|
const value = (expr as any)[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 as number]: true };
|
|
default:
|
|
return { [expr as any]: true };
|
|
}
|
|
}
|
|
|
|
export function setClass(this: HTMLElement, val: any) {
|
|
val = val === "" ? {} : toClassObj(val);
|
|
// add classes
|
|
const cl = this.classList;
|
|
for (let c in val) {
|
|
tokenListAdd.call(cl, c);
|
|
}
|
|
}
|
|
|
|
export function updateClass(this: HTMLElement, val: any, oldVal: any) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function makePropSetter(name: string): Setter<HTMLElement> {
|
|
return function setProp(this: HTMLElement, value: any) {
|
|
// support 0, fallback to empty string for other falsy values
|
|
(this as any)[name] = value === 0 ? 0 : value ? value.valueOf() : "";
|
|
};
|
|
}
|
|
|
|
export function isProp(tag: string, key: string): boolean {
|
|
switch (tag) {
|
|
case "input":
|
|
return (
|
|
key === "checked" ||
|
|
key === "indeterminate" ||
|
|
key === "value" ||
|
|
key === "readonly" ||
|
|
key === "disabled"
|
|
);
|
|
case "option":
|
|
return key === "selected" || key === "disabled";
|
|
case "textarea":
|
|
return key === "value" || key === "readonly" || key === "disabled";
|
|
case "select":
|
|
return key === "value" || key === "disabled";
|
|
case "button":
|
|
case "optgroup":
|
|
return key === "disabled";
|
|
}
|
|
return false;
|
|
}
|