diff --git a/packages/@lwc/engine/src/env/node.ts b/packages/@lwc/engine/src/env/node.ts index 549f0d0046..03da9f115b 100644 --- a/packages/@lwc/engine/src/env/node.ts +++ b/packages/@lwc/engine/src/env/node.ts @@ -43,6 +43,16 @@ const nodeValueSetter: (this: Node, value: string) => void = nodeValueDescriptor const nodeValueGetter: (this: Node) => string = nodeValueDescriptor.get!; +const isConnected = hasOwnProperty.call(Node.prototype, 'isConnected') ? + getOwnPropertyDescriptor(Node.prototype, 'isConnected')!.get! : + function(this: Node): boolean { // IE11 + return ( + (compareDocumentPosition.call(document, this) & + DOCUMENT_POSITION_CONTAINED_BY) !== + 0 + ); + }; + export { // Node.prototype compareDocumentPosition, @@ -58,6 +68,7 @@ export { nodeValueGetter, nodeValueSetter, cloneNode, + isConnected, // Node DOCUMENT_POSITION_CONTAINS, diff --git a/packages/@lwc/engine/src/faux-shadow/shadow-root.ts b/packages/@lwc/engine/src/faux-shadow/shadow-root.ts index 5d99417f6e..554f1dd3b0 100644 --- a/packages/@lwc/engine/src/faux-shadow/shadow-root.ts +++ b/packages/@lwc/engine/src/faux-shadow/shadow-root.ts @@ -12,13 +12,14 @@ import { getInternalField, setInternalField, createFieldName } from "../shared/f import { getTextContent } from "../3rdparty/polymer/text-content"; import { createStaticNodeList } from "../shared/static-node-list"; import { DocumentPrototypeActiveElement, elementFromPoint, createComment } from "../env/document"; -import { compareDocumentPosition, DOCUMENT_POSITION_CONTAINED_BY, parentElementGetter } from "../env/node"; +import { compareDocumentPosition, DOCUMENT_POSITION_CONTAINED_BY, parentElementGetter, textContextSetter, isConnected } from "../env/node"; import { isNativeShadowRootAvailable } from "../env/dom"; import { createStaticHTMLCollection } from "../shared/static-html-collection"; import { getOuterHTML } from "../3rdparty/polymer/outer-html"; import { retarget } from "../3rdparty/polymer/retarget"; import { pathComposer } from "../3rdparty/polymer/path-composer"; import { getInternalChildNodes } from "./node"; +import { innerHTMLSetter } from "../env/element"; const HostKey = createFieldName('host'); const ShadowRootKey = createFieldName('shadowRoot'); @@ -295,11 +296,7 @@ const NodePatchDescriptors = { enumerable: true, configurable: true, get(this: SyntheticShadowRootInterface) { - return ( - (compareDocumentPosition.call(document, getHost(this)) & - DOCUMENT_POSITION_CONTAINED_BY) !== - 0 - ); + return isConnected.call(getHost(this)); }, }, nextSibling: { @@ -369,6 +366,10 @@ const NodePatchDescriptors = { } return textContent; }, + set(this: SyntheticShadowRootInterface, v: string) { + const host = getHost(this); + textContextSetter.call(host, v); + } }, getRootNode: { writable: true, @@ -392,6 +393,10 @@ const ElementPatchDescriptors = { } return innerHTML; }, + set(this: SyntheticShadowRootInterface, v: string) { + const host = getHost(this); + innerHTMLSetter.call(host, v); + }, }, localName: { enumerable: true, diff --git a/packages/@lwc/engine/src/framework/restrictions.ts b/packages/@lwc/engine/src/framework/restrictions.ts index 5897531d51..e16beb06e7 100644 --- a/packages/@lwc/engine/src/framework/restrictions.ts +++ b/packages/@lwc/engine/src/framework/restrictions.ts @@ -46,7 +46,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions appendChild: { value(this: Node, aChild: Node) { if (this instanceof Element && options.isPortal !== true) { - assert.logError(`appendChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element); + assert.logError(`appendChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } return appendChild.call(this, aChild); }, @@ -57,7 +57,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions insertBefore: { value(this: Node, newNode: Node, referenceNode: Node) { if (this instanceof Element && options.isPortal !== true) { - assert.logError(`insertBefore is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element); + assert.logError(`insertBefore is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } return insertBefore.call(this, newNode, referenceNode); }, @@ -68,7 +68,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions removeChild: { value(this: Node, aChild: Node) { if (this instanceof Element && options.isPortal !== true) { - assert.logError(`removeChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element); + assert.logError(`removeChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } return removeChild.call(this, aChild); }, @@ -79,7 +79,7 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions replaceChild: { value(this: Node, newChild: Node, oldChild: Node) { if (this instanceof Element && options.isPortal !== true) { - assert.logError(`replaceChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element); + assert.logError(`replaceChild is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } return replaceChild.call(this, newChild, oldChild); }, @@ -92,10 +92,8 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions return originalNodeValueDescriptor.get!.call(this); }, set(this: Node, value: string) { - if (process.env.NODE_ENV !== 'production') { - if (this instanceof Element && options.isPortal !== true) { - assert.logError(`nodeValue is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element); - } + if (this instanceof Element && options.isPortal !== true) { + assert.logError(`nodeValue is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } originalNodeValueDescriptor.set!.call(this, value); } @@ -105,10 +103,8 @@ function getNodeRestrictionsDescriptors(node: Node, options: RestrictionsOptions return originalTextContentDescriptor.get!.call(this); }, set(this: Node, value: string) { - if (process.env.NODE_ENV !== 'production') { - if (this instanceof Element && options.isPortal !== true) { - assert.logError(`textContent is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this as Element); - } + if (this instanceof Element && options.isPortal !== true) { + assert.logError(`textContent is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } originalTextContentDescriptor.set!.call(this, value); } @@ -124,22 +120,31 @@ function getElementRestrictionsDescriptors(elm: HTMLElement, options: Restrictio } const descriptors: PropertyDescriptorMap = getNodeRestrictionsDescriptors(elm, options); const originalInnerHTMLDescriptor = getPropertyDescriptor(elm, 'innerHTML')!; + const originalOuterHTMLDescriptor = getPropertyDescriptor(elm, 'outerHTML')!; assign(descriptors, { innerHTML: { get(): string { return originalInnerHTMLDescriptor.get!.call(this); }, set(this: HTMLElement, value: string) { - if (process.env.NODE_ENV !== 'production') { - if (options.isPortal !== true) { - assert.logError(`innerHTML is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); - } + if (options.isPortal !== true) { + assert.logError(`innerHTML is disallowed in Element unless \`lwc:dom="manual"\` directive is used in the template.`, this); } return originalInnerHTMLDescriptor.set!.call(this, value); }, enumerable: true, configurable: true, - } + }, + outerHTML: { + get(this: HTMLElement): string { + return originalOuterHTMLDescriptor.get!.call(this); + }, + set(this: HTMLElement, value: string) { + throw new TypeError(`Invalid attempt to set outerHTML on Element.`); + }, + enumerable: true, + configurable: true, + }, }); return descriptors; } @@ -156,7 +161,29 @@ function getShadowRootRestrictionsDescriptors(sr: ShadowRoot, options: Restricti const originalQuerySelectorAll = sr.querySelectorAll; const originalAddEventListener = sr.addEventListener; const descriptors: PropertyDescriptorMap = getNodeRestrictionsDescriptors(sr, options); + const originalInnerHTMLDescriptor = getPropertyDescriptor(sr, 'innerHTML')!; + const originalTextContentDescriptor = getPropertyDescriptor(sr, 'textContent')!; assign(descriptors, { + innerHTML: { + get(this: ShadowRoot): string { + return originalInnerHTMLDescriptor.get!.call(this); + }, + set(this: ShadowRoot, value: string) { + throw new TypeError(`Invalid attempt to set innerHTML on ShadowRoot.`); + }, + enumerable: true, + configurable: true, + }, + textContent: { + get(this: ShadowRoot): string { + return originalTextContentDescriptor.get!.call(this); + }, + set(this: ShadowRoot, value: string) { + throw new TypeError(`Invalid attempt to set textContent on ShadowRoot.`); + }, + enumerable: true, + configurable: true, + }, addEventListener: { value(this: ShadowRoot, type: string) { assert.invariant(!isRendering, `${vmBeingRendered}.render() method has side effects on the state of ${toString(sr)} by adding an event listener for "${type}".`); @@ -281,9 +308,42 @@ function getCustomElementRestrictionsDescriptors(elm: HTMLElement, options: Rest } const descriptors: PropertyDescriptorMap = getNodeRestrictionsDescriptors(elm, options); const originalAddEventListener = elm.addEventListener; + const originalInnerHTMLDescriptor = getPropertyDescriptor(elm, 'innerHTML')!; + const originalOuterHTMLDescriptor = getPropertyDescriptor(elm, 'outerHTML')!; + const originalTextContentDescriptor = getPropertyDescriptor(elm, 'textContent')!; return assign(descriptors, { + innerHTML: { + get(this: HTMLElement): string { + return originalInnerHTMLDescriptor.get!.call(this); + }, + set(this: HTMLElement, value: string) { + throw new TypeError(`Invalid attempt to set innerHTML on HTMLElement.`); + }, + enumerable: true, + configurable: true, + }, + outerHTML: { + get(this: HTMLElement): string { + return originalOuterHTMLDescriptor.get!.call(this); + }, + set(this: HTMLElement, value: string) { + throw new TypeError(`Invalid attempt to set outerHTML on HTMLElement.`); + }, + enumerable: true, + configurable: true, + }, + textContent: { + get(this: HTMLElement): string { + return originalTextContentDescriptor.get!.call(this); + }, + set(this: HTMLElement, value: string) { + throw new TypeError(`Invalid attempt to set textContent on HTMLElement.`); + }, + enumerable: true, + configurable: true, + }, addEventListener: { - value(this: ShadowRoot, type: string) { + value(this: HTMLElement, type: string) { assert.invariant(!isRendering, `${vmBeingRendered}.render() method has side effects on the state of ${toString(elm)} by adding an event listener for "${type}".`); return originalAddEventListener.apply(this, arguments); }