From 3e9955ddde9faa0dca8383627c45fb8b82e148fb Mon Sep 17 00:00:00 2001 From: David Turissini Date: Wed, 14 Nov 2018 14:22:33 -0800 Subject: [PATCH 01/11] fix(engine): implement polymer retargeting --- packages/lwc-engine/src/env/node.ts | 5 + packages/lwc-engine/src/faux-shadow/events.ts | 188 +++++------------- 2 files changed, 54 insertions(+), 139 deletions(-) diff --git a/packages/lwc-engine/src/env/node.ts b/packages/lwc-engine/src/env/node.ts index 655a3f1f0b..6126d2ae6a 100644 --- a/packages/lwc-engine/src/env/node.ts +++ b/packages/lwc-engine/src/env/node.ts @@ -5,6 +5,8 @@ const { DOCUMENT_POSITION_CONTAINS, DOCUMENT_POSITION_PRECEDING, DOCUMENT_POSITION_FOLLOWING, + + DOCUMENT_FRAGMENT_NODE, } = Node; const { @@ -54,4 +56,7 @@ export { DOCUMENT_POSITION_CONTAINED_BY, DOCUMENT_POSITION_PRECEDING, DOCUMENT_POSITION_FOLLOWING, + + // Node Types + DOCUMENT_FRAGMENT_NODE }; diff --git a/packages/lwc-engine/src/faux-shadow/events.ts b/packages/lwc-engine/src/faux-shadow/events.ts index d4d2c642f3..1ec6fc1e00 100644 --- a/packages/lwc-engine/src/faux-shadow/events.ts +++ b/packages/lwc-engine/src/faux-shadow/events.ts @@ -4,18 +4,13 @@ import { removeEventListener, } from "../env/element"; import { - parentNodeGetter, - DOCUMENT_POSITION_CONTAINS, compareDocumentPosition, DOCUMENT_POSITION_CONTAINED_BY, + DOCUMENT_FRAGMENT_NODE, } from "../env/node"; -import { - getNodeNearestOwnerKey, - getNodeKey, -} from "./node"; import { ArraySlice, ArraySplice, ArrayIndexOf, create, ArrayPush, isUndefined, isFunction, defineProperties, toString, forEach, defineProperty, isFalse } from "../shared/language"; -import { isNodeSlotted, getRootNodeGetter } from "./traverse"; -import { getHost, SyntheticShadowRootInterface } from "./shadow-root"; +import { getRootNodeGetter } from "./traverse"; +import { getHost, SyntheticShadowRootInterface, SyntheticShadowRoot, getShadowRoot } from "./shadow-root"; import { eventCurrentTargetGetter, eventTargetGetter } from "../env/dom"; interface WrappedListener extends EventListener { @@ -47,140 +42,56 @@ function getRootNodeHost(node: Node, options): Node { return rootNode; } +function pathComposer(startNode: Node, composed: boolean): Node[] { + let composedPath: HTMLElement[] = []; + let current = startNode; + let startRoot = startNode as any === window ? window : getRootNodeGetter.call(startNode); + while (current) { + composedPath.push(current as HTMLElement); + if ((current as HTMLElement).assignedSlot) { + current = (current as HTMLElement).assignedSlot as HTMLSlotElement; + } else if ((current as HTMLElement).nodeType === DOCUMENT_FRAGMENT_NODE && (current as ShadowRoot).host && (composed || current !== startRoot)) { + current = (current as ShadowRoot).host as HTMLElement + } else { + current = (current as HTMLElement).parentNode as any; + } + } + // event composedPath includes window when startNode's ownerRoot is document + if (composedPath[composedPath.length - 1] as any === document) { + composedPath.push(window as any); + } + return composedPath; +} + + +function retarget(refNode: Node, path: Node[]): EventTarget | undefined { + // If ANCESTOR's root is not a shadow root or ANCESTOR's root is BASE's + // shadow-including inclusive ancestor, return ANCESTOR. + let refNodePath = pathComposer(refNode, true); + let p$ = path; + for (let i = 0, ancestor, lastRoot, root, rootIdx; i < p$.length; i++) { + ancestor = p$[i]; + root = ancestor === window ? window : getRootNodeGetter.call(ancestor); + if (root !== lastRoot) { + rootIdx = refNodePath.indexOf(root); + lastRoot = root; + } + if (!(root instanceof SyntheticShadowRoot) || rootIdx > -1) { + return ancestor; + } + } +} + const EventPatchDescriptors: PropertyDescriptorMap = { target: { get(this: Event): EventTarget { - const currentTarget: EventTarget = eventCurrentTargetGetter.call(this); + const originalCurrentTarget: EventTarget = eventCurrentTargetGetter.call(this); const originalTarget: EventTarget = eventTargetGetter.call(this); - - // Handle cases where the currentTarget is null (for async events), - // and when it's not owned by a custom element - if (!(currentTarget instanceof Node) - || (getRootNodeGetter.call(currentTarget, GET_ROOT_NODE_CONFIG_FALSE) === document - && isUndefined(getNodeKey(currentTarget)))) { - // the event was inspected asynchronously, in which case we need to return the - // top custom element that belongs to the body. - let outerMostElement = originalTarget; - let parentNode; - while ((parentNode = parentNodeGetter.call(outerMostElement)) && !isUndefined(getNodeNearestOwnerKey(outerMostElement as Node))) { - outerMostElement = parentNode; - } - - // This value will always be the root LWC node. - // There is a chance that this value will be accessed - // inside of an async event handler in the component tree, - // but because we don't know if it is being accessed - // inside the tree or outside the tree, we do not patch. - return outerMostElement; - } - - // Handle cases where the target is detached from the currentTarget node subtree - if (isFalse(compareDocumentPosition.call(originalTarget, currentTarget) & DOCUMENT_POSITION_CONTAINS)) { - // In this case, the original target is in a detached root, making it - // impossible to retarget (unless we figure out something clever). - return originalTarget; - } - const eventContext = eventToContextMap.get(this); - - // Retarget to currentTarget if the listener was added to a custom element. - if (eventContext === EventListenerContext.CUSTOM_ELEMENT_LISTENER) { - return currentTarget as Element; - } - - // We need to determine 2 things in order to retarget correctly: - // 1) What VM context was the listener added? (e.g. in what VM context was addEventListener called in). - // 2) What is the event's original target's relationship to 1, is it owned by the vm, or was it slotted? - - // Determining Number 1: - // In most cases, the VM context maps to the currentTarget's owner VM. This will correspond to the custom element: - // - // // x-parent.html - // - // - // In the case of this.template.addEventListener, the VM context needs to be the custom element's VM, NOT the owner VM. - // - // // x-parent.js - // connectedCallback() { - // The event below is attached to x-parent's shadow root. - // Under the hood, we add the event listener to the custom element. - // Because template events happen INSIDE the custom element's shadow, - // we CANNOT get the owner VM. Instead, we must get the custom element's VM instead. - // this.template.addEventListener('click', () => {}); - // } - const myCurrentShadowKey = (eventContext === EventListenerContext.SHADOW_ROOT_LISTENER) ? getNodeKey(currentTarget as Node) : getNodeNearestOwnerKey(currentTarget as Node); - - // Resolving the host of the shadow that is being retargeted (which is based on the current target) - // with this value, we can check if any element in the path was slotted (directly or indirectly). - // Directly means: it is a slotted element - // Indirectly means: it is a qualifying child of a slotted element - - const host = (eventContext === EventListenerContext.SHADOW_ROOT_LISTENER) - ? currentTarget - : getRootNodeHost(currentTarget, GET_ROOT_NODE_CONFIG_FALSE); - - // Determining Number 2: - // Because we only support bubbling and we are already inside of an event, we know that the original event target - // is a child of the currentTarget. The key here, is that we have to determine if the event is coming from an - // element inside of the attached shadow context (#1 above) or was slotted (#2). - // We determine this by traversing up the DOM until either 1) We come across an element that has the same VM as #1 - // Or we come across an element that was slotted inside a shadow's slot element from #1. - // - // If we come across an element that has the same VM as #1, we have an element that was rendered inside #1 template: - // - //