UNPKG

react-reverse-portal

Version:
195 lines 9.35 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); import * as React from 'react'; import * as ReactDOM from 'react-dom'; // Internally, the portalNode must be for either HTML or SVG elements var ELEMENT_TYPE_HTML = 'html'; var ELEMENT_TYPE_SVG = 'svg'; // ReactDOM can handle several different namespaces, but they're not exported publicly // https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/shared/DOMNamespaces.js#L8-L10 var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; var validateElementType = function (domElement, elementType) { var _a, _b, _c; var ownerDocument = ((_a = domElement.ownerDocument) !== null && _a !== void 0 ? _a : document); // Cast document to `any` because Typescript doesn't know about the legacy `Document.parentWindow` field, and also // doesn't believe `Window.HTMLElement`/`Window.SVGElement` can be used in instanceof tests. var ownerWindow = (_c = (_b = ownerDocument.defaultView) !== null && _b !== void 0 ? _b : ownerDocument.parentWindow) !== null && _c !== void 0 ? _c : window; // `parentWindow` for IE8 and earlier switch (elementType) { case ELEMENT_TYPE_HTML: return domElement instanceof ownerWindow.HTMLElement; case ELEMENT_TYPE_SVG: return domElement instanceof ownerWindow.SVGElement; default: throw new Error("Unrecognized element type \"".concat(elementType, "\" for validateElementType.")); } }; // This is the internal implementation: the public entry points set elementType to an appropriate value var createPortalNode = function (elementType, options) { var _a, _b; var initialProps = {}; var parent; var lastPlaceholder; var element; switch (elementType) { case ELEMENT_TYPE_HTML: element = document.createElement((_a = options === null || options === void 0 ? void 0 : options.containerElement) !== null && _a !== void 0 ? _a : 'div'); break; case ELEMENT_TYPE_SVG: element = document.createElementNS(SVG_NAMESPACE, (_b = options === null || options === void 0 ? void 0 : options.containerElement) !== null && _b !== void 0 ? _b : 'g'); break; default: throw new Error("Invalid element type \"".concat(elementType, "\" for createPortalNode: must be \"html\" or \"svg\".")); } if (options && typeof options === "object" && options.attributes) { for (var _i = 0, _c = Object.entries(options.attributes); _i < _c.length; _i++) { var _d = _c[_i], key = _d[0], value = _d[1]; element.setAttribute(key, value); } } var portalNode = { element: element, elementType: elementType, setPortalProps: function (props) { initialProps = props; }, getInitialPortalProps: function () { return initialProps; }, mount: function (newParent, newPlaceholder) { if (newPlaceholder === lastPlaceholder) { // Already mounted - noop. return; } portalNode.unmount(); // To support SVG and other non-html elements, the portalNode's elementType needs to match // the elementType it's being rendered into if (newParent !== parent) { if (!validateElementType(newParent, elementType)) { throw new Error("Invalid element type for portal: \"".concat(elementType, "\" portalNodes must be used with ").concat(elementType, " elements, but OutPortal is within <").concat(newParent.tagName, ">.")); } } newParent.replaceChild(portalNode.element, newPlaceholder); parent = newParent; lastPlaceholder = newPlaceholder; }, unmount: function (expectedPlaceholder) { if (expectedPlaceholder && expectedPlaceholder !== lastPlaceholder) { // Skip unmounts for placeholders that aren't currently mounted // They will have been automatically unmounted already by a subsequent mount() return; } if (parent && lastPlaceholder) { parent.replaceChild(lastPlaceholder, portalNode.element); parent = undefined; lastPlaceholder = undefined; } } }; return portalNode; }; var InPortal = /** @class */ (function (_super) { __extends(InPortal, _super); function InPortal(props) { var _this = _super.call(this, props) || this; _this.addPropsChannel = function () { Object.assign(_this.props.node, { setPortalProps: function (props) { // Rerender the child node here if/when the out portal props change _this.setState({ nodeProps: props }); } }); }; _this.state = { nodeProps: _this.props.node.getInitialPortalProps(), }; return _this; } InPortal.prototype.componentDidMount = function () { this.addPropsChannel(); }; InPortal.prototype.componentDidUpdate = function () { this.addPropsChannel(); }; InPortal.prototype.render = function () { var _this = this; var _a = this.props, children = _a.children, node = _a.node; return ReactDOM.createPortal(React.Children.map(children, function (child) { if (!React.isValidElement(child)) return child; return React.cloneElement(child, _this.state.nodeProps); }), node.element); }; return InPortal; }(React.PureComponent)); var OutPortal = /** @class */ (function (_super) { __extends(OutPortal, _super); function OutPortal(props) { var _this = _super.call(this, props) || this; _this.placeholderNode = React.createRef(); _this.passPropsThroughPortal(); return _this; } OutPortal.prototype.passPropsThroughPortal = function () { var propsForTarget = Object.assign({}, this.props, { node: undefined }); this.props.node.setPortalProps(propsForTarget); }; OutPortal.prototype.componentDidMount = function () { var node = this.props.node; this.currentPortalNode = node; var placeholder = this.placeholderNode.current; var parent = placeholder.parentNode; node.mount(parent, placeholder); this.passPropsThroughPortal(); }; OutPortal.prototype.componentDidUpdate = function () { // We re-mount on update, just in case we were unmounted (e.g. by // a second OutPortal, which has now been removed) var node = this.props.node; // If we're switching portal nodes, we need to clean up the current one first. if (this.currentPortalNode && node !== this.currentPortalNode) { this.currentPortalNode.unmount(this.placeholderNode.current); this.currentPortalNode.setPortalProps({}); this.currentPortalNode = node; } var placeholder = this.placeholderNode.current; var parent = placeholder.parentNode; node.mount(parent, placeholder); this.passPropsThroughPortal(); }; OutPortal.prototype.componentWillUnmount = function () { var node = this.props.node; node.unmount(this.placeholderNode.current); node.setPortalProps({}); }; OutPortal.prototype.render = function () { // Render a placeholder to the DOM, so we can get a reference into // our location in the DOM, and swap it out for the portaled node. var tagName = this.props.node.element.tagName; // SVG tagName is lowercase and case sensitive, HTML is uppercase and case insensitive. // React.createElement expects lowercase first letter to treat as non-component element. // (Passing uppercase type won't break anything, but React warns otherwise:) // https://github.com/facebook/react/blob/8039f1b2a05d00437cd29707761aeae098c80adc/CHANGELOG.md?plain=1#L1984 var type = this.props.node.elementType === ELEMENT_TYPE_HTML ? tagName.toLowerCase() : tagName; return React.createElement(type, { ref: this.placeholderNode }); }; return OutPortal; }(React.PureComponent)); var createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML); var createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG); export { createHtmlPortalNode, createSvgPortalNode, InPortal, OutPortal, }; //# sourceMappingURL=index.js.map