mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
6f23b18cab
Previously, when trying to mount a portal into a target that would be mounted in the same render as the portal itself, the portal content could not be mounted correctly. In a previous attempt to fix it, a mistake was made while writing the test causing us to incorrectly believe in now worked, when in fact, it would just move the portal content to the end of its parent but without changing it. This commit fixes the issue by making the `moveBeforeDOMNode` method of VNodes accept a second optional parameter which is the parent in which the element should be moved, defaulting to the current parent. This results in identical behaviour when a parent is not specified, but when it is, the VNode is "reparented" to the passed parent, which is what is now done by the Portal
249 lines
7.3 KiB
TypeScript
249 lines
7.3 KiB
TypeScript
import type { VNode } from "./index";
|
|
|
|
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
|
|
const nodeProto = Node.prototype;
|
|
|
|
const nodeInsertBefore = nodeProto.insertBefore;
|
|
const nodeAppendChild = nodeProto.appendChild;
|
|
const nodeRemoveChild = nodeProto.removeChild;
|
|
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// List Node
|
|
// -----------------------------------------------------------------------------
|
|
|
|
class VList {
|
|
children: VNode[];
|
|
anchor: Node | undefined;
|
|
parentEl?: HTMLElement | undefined;
|
|
isOnlyChild?: boolean | undefined;
|
|
|
|
constructor(children: VNode[]) {
|
|
this.children = children;
|
|
}
|
|
|
|
mount(parent: HTMLElement, afterNode: Node | null) {
|
|
const children = this.children;
|
|
const _anchor = document.createTextNode("");
|
|
this.anchor = _anchor;
|
|
nodeInsertBefore.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: Node | null, 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: VList | null, afterNode: Node | null) {
|
|
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: VList, withBeforeRemove: boolean) {
|
|
if (this === other) {
|
|
return;
|
|
}
|
|
const ch1 = this.children;
|
|
const ch2: VNode[] = 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: any = 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 as any;
|
|
}
|
|
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.call(parentEl, anchor!);
|
|
}
|
|
}
|
|
|
|
firstNode(): Node | undefined {
|
|
const child = this.children[0];
|
|
return child ? child.firstNode() : undefined;
|
|
}
|
|
|
|
toString(): string {
|
|
return this.children.map((c) => c!.toString()).join("");
|
|
}
|
|
}
|
|
|
|
export function list(children: VNode[]): VNode<VList> {
|
|
return new VList(children);
|
|
}
|
|
|
|
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
|
|
let mapping: any = {};
|
|
for (let i = startIdx1; i <= endIdx2; i++) {
|
|
mapping[ch1[i].key] = i;
|
|
}
|
|
return mapping;
|
|
}
|