(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom'], factory) : (global = global || self, factory(global.Remount = {}, global.React, global.ReactDOM)); }(this, function (exports, React, ReactDOM) { 'use strict'; function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } /* global HTMLElement */ /* * Adapted from https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs@2.0.4/custom-elements-es5-adapter.js * Rolling this in so we don't need another polyfill. */ function inject() { if (window.HTMLElement && window.HTMLElement._babelES5Adapter || void 0 === window.Reflect || void 0 === window.customElements || window.customElements.hasOwnProperty('polyfillWrapFlushCallback')) { return; } var a = HTMLElement; window.HTMLElement = function () { return Reflect.construct(a, [], this.constructor); }; HTMLElement.prototype = a.prototype; HTMLElement.prototype.constructor = HTMLElement; Object.setPrototypeOf(HTMLElement, a); HTMLElement._babelES5Adapter = true; } /** * The name of this strategy. * @type string */ var name = 'CustomElements'; /** * Registers a custom element. * * This creates a custom element (ie, a subclass of `window.HTMLElement`) and * registers it (ie, `window.customElements.define`). * * Events will be triggered when something interesting happens. * * @example * defineElement( * { component: Tooltip }, * 'x-tooltip', * { onUpdate, onUnmount } * ) * * @private * @param {ElementSpec} elSpec * @param {string} elName * @param {ElementEvents} events */ function defineElement(elSpec, elName, events) { var onUpdate = events.onUpdate, onUnmount = events.onUnmount, onMount = events.onMount; inject(); var attributes = elSpec.attributes || []; var ComponentElement = /*#__PURE__*/ function (_HTMLElement) { _inherits(ComponentElement, _HTMLElement); function ComponentElement() { _classCallCheck(this, ComponentElement); return _possibleConstructorReturn(this, _getPrototypeOf(ComponentElement).apply(this, arguments)); } _createClass(ComponentElement, [{ key: "connectedCallback", value: function connectedCallback() { this._mountPoint = createMountPoint(this, elSpec); onMount(this, this._mountPoint); } }, { key: "disconnectedCallback", value: function disconnectedCallback() { if (!this._mountPoint) { return; } onUnmount(this, this._mountPoint); } }, { key: "attributeChangedCallback", value: function attributeChangedCallback() { if (!this._mountPoint) { return; } onUpdate(this, this._mountPoint); } }], [{ key: "observedAttributes", get: function get() { return ['props-json'].concat(_toConsumableArray(attributes)); } }]); return ComponentElement; }(_wrapNativeSuper(HTMLElement)); // Supress warning when quiet mode is on if (elSpec.quiet && window.customElements.get(elName)) { return; } window.customElements.define(elName, ComponentElement); } function isSupported() { return !!(window.customElements && window.customElements.define); } /** * Creates a `` element that serves as the mounting point for React * components. If `shadow: true` is requested, it'll attach a shadow node. * * @private * @param {HTMLElement} element * @param {ElementSpec} elSpec */ function createMountPoint(element, elSpec) { var shadow = elSpec.shadow; if (shadow && element.attachShadow) { var mountPoint = document.createElement('span'); element.attachShadow({ mode: 'open' }).appendChild(mountPoint); return mountPoint; } else { return element; } } /** * Check if Shadow DOM is supported. */ function supportsShadow() { return !!(document && document.body && document.body.attachShadow); } var CustomElementsStrategy = /*#__PURE__*/Object.freeze({ name: name, defineElement: defineElement, isSupported: isSupported, supportsShadow: supportsShadow }); // @ts-check /** * Some implementations of MutationObserver don't have .forEach, * so we need our own `forEach` shim. This is usually the case with * polyfilled environments. * * @type { import('./types').Each } */ function each( /** @type any */ list, /** @type any */ fn) { for (var i = 0, len = list.length; i < len; i++) { fn(list[i]); } } // @ts-check /** * The name of this strategy. * @type string */ var name$1 = 'MutationObserver'; /** * List of observers tags. * @type ObserverList */ var observers = {}; function isSupported$1() { return 'MutationObserver' in window; } /** * Defines a custom element. * * @example * defineElement( * { component: MyComponent }, * 'my-div', * { * onMount: () => {}, * onUpdate: () => {}, * onUnmount: () => {}, * } * ) * * @private * @param {ElementSpec} elSpec * @param {string} elName * @param {ElementEvents} events */ function defineElement$1(elSpec, elName, events) { elName = elName.toLowerCase(); // Maintain parity with what would happen in Custom Elements mode if (!isValidName(elName)) { if (elSpec.quiet) { return; } throw new Error("Remount: \"".concat(elName, "\" is not a valid custom element elName")); } if (observers[elName]) { if (elSpec.quiet) { return; } throw new Error("Remount: \"".concat(elName, "\" is already registered")); } var observer = new MutationObserver( /** @type MutationCallback */ function (mutations) { each(mutations, function ( /** @type MutationRecord */ mutation) { each(mutation.addedNodes, function ( /** @type Node */ node) { if (isElement(node)) { checkForMount(node, elName, events); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); observers[name$1] = /* true */ observer; window.addEventListener('DOMContentLoaded', function () { var nodes = document.getElementsByTagName(name$1); each(nodes, function ( /** @type HTMLElement */ node) { return checkForMount(node, name$1, events); }); }); } /** * Checks if this new element should fire an `onUpdate` hook. * Recurses down to its descendant nodes. * * @param {HTMLElement} node * @param {string} elName * @param {ElementEvents} events */ function checkForMount(node, elName, events) { if (node.nodeName.toLowerCase() === elName) { // It's a match! events.onMount(node, node); observeForUpdates(node, events); observeForRemoval(node, events); } else if (node.children && node.children.length) { // Recurse down into the other additions each(node.children, function ( /** @type HTMLElement */ subnode) { if (isElement(subnode)) { checkForMount(subnode, elName, events); } }); } } /** * Observes for any changes in attributes. * * @param {Element} node * @param {ElementEvents} events */ function observeForUpdates(node, events) { var onUpdate = events.onUpdate; var observer = new MutationObserver( /** @type MutationCallback */ function (mutations) { each(mutations, function ( /** @type MutationRecord */ mutation) { var targetNode = mutation.target; if (isElement(targetNode)) { onUpdate(targetNode, targetNode); } }); }); observer.observe(node, { attributes: true }); } /** * Observes a node's parent to wait until the node is removed * @param {HTMLElement} node * @param {ElementEvents} events */ function observeForRemoval(node, events) { var onUnmount = events.onUnmount; var parent = node.parentNode; // Not sure when this can happen, but let's add this for type safety if (!parent) { return; } var observer = new MutationObserver( /** @type MutationCallback */ function (mutations) { each(mutations, function ( /** @type MutationRecord */ mutation) { each(mutation.removedNodes, function ( /** @type Node */ subnode) { if (node !== subnode) { return; } if (isElement(node)) { // @ts-ignore TypeScript expects 0 arguments...? observer.disconnect(parent); onUnmount(node, node); } }); }); }); observer.observe(parent, { childList: true, subtree: true }); } /** * Validate a custom tag. * * Since Remount can work with either Custom Elements or MutationObserver API's, * it'd be wise if we rejected element names that won't work in Custom Elements * mode (even if we're using MutationObserver mode). * * @param {string} elName * @returns {boolean} * * @example * isValidName('div') // => false * isValidName('my-div') // => true * isValidName('123-456') // => false * isValidName('my-123') // => true * * @private */ function isValidName(elName) { return !!(elName.indexOf('-') !== -1 && elName.match(/^[a-z][a-z0-9-]*$/)); } /** * Shadow DOM is not supported with the Mutation Observer strategy. */ function supportsShadow$1() { return false; } /** * Checks if a given Node is an HTMLElement. * * It's possible that a mutation's `addedNodes` return something that isn't an * HTMLElement. * * @param {any} node * @returns {node is HTMLElement} */ function isElement(node) { if (node) { return true; } return false; } var MutationObserverStrategy = /*#__PURE__*/Object.freeze({ name: name$1, observers: observers, isSupported: isSupported$1, defineElement: defineElement$1, supportsShadow: supportsShadow$1 }); var reactEvents = ["onAbort", "onAnimationCancel", "onAnimationEnd", "onAnimationIteration", "onAuxClick", "onBlur", "onChange", "onClick", "onClose", "onContextMenu", "onDoubleClick", "onError", "onFocus", "onGotPointerCapture", "onInput", "onKeyDown", "onKeyPress", "onKeyUp", "onLoad", "onLoadEnd", "onLoadStart", "onLostPointerCapture", "onMouseDown", "onMouseMove", "onMouseOut", "onMouseOver", "onMouseUp", "onPointerCancel", "onPointerDown", "onPointerEnter", "onPointerLeave", "onPointerMove", "onPointerOut", "onPointerOver", "onPointerUp", "onReset", "onResize", "onScroll", "onSelect", "onSelectionChange", "onSelectStart", "onSubmit", "onTouchCancel", "onTouchMove", "onTouchStart", "onTransitionCancel", "onTransitionEnd", "onDrag", "onDragEnd", "onDragEnter", "onDragExit", "onDragLeave", "onDragOver", "onDragStart", "onDrop", "onFocusOut"]; var divergentNativeEvents = { onDoubleClick: 'dblclick' }; var mimickedReactEvents = { onInput: 'onChange', onFocusOut: 'onBlur', onSelectionChange: 'onSelect' }; var reactShadowDomRetargetEvents = function retargetEvents(shadowRoot) { reactEvents.forEach(function (reactEventName) { var nativeEventName = getNativeEventName(reactEventName); shadowRoot.addEventListener(nativeEventName, function (event) { var path = event.path || (event.composedPath && event.composedPath()) || composedPath(event.target); for (var i = 0; i < path.length; i++) { var el = path[i]; var reactComponent = findReactComponent(el); var props = findReactProps(reactComponent); if (reactComponent && props) { dispatchEvent(event, reactEventName, props); } if (reactComponent && props && mimickedReactEvents[reactEventName]) { dispatchEvent(event, mimickedReactEvents[reactEventName], props); } if (event.cancelBubble) { break; } if (el === shadowRoot) { break; } } }, false); }); }; function findReactComponent(item) { for (var key in item) { if (item.hasOwnProperty(key) && key.indexOf('_reactInternal') !== -1) { return item[key]; } } } function findReactProps(component) { if (!component) return undefined; if (component.memoizedProps) return component.memoizedProps; // React 16 Fiber if (component._currentElement && component._currentElement.props) return component._currentElement.props; // React <=15 } function dispatchEvent(event, eventType, componentProps) { if (componentProps[eventType]) { componentProps[eventType](event); } } function getNativeEventName(reactEventName) { if (divergentNativeEvents[reactEventName]) { return divergentNativeEvents[reactEventName]; } return reactEventName.replace(/^on/, '').toLowerCase(); } function composedPath(el) { var path = []; while (el) { path.push(el); if (el.tagName === 'HTML') { path.push(document); path.push(window); return path; } el = el.parentElement; } } // @ts-check /** * @param {ElementSpec} elSpec * @param {HTMLElement} mountPoint * @param {object} props * @param {HTMLElement | null} element */ function mount(elSpec, mountPoint, props, element) { return update(elSpec, mountPoint, props, element); } /** * Updates a custom element by calling `ReactDOM.render()`. * @private * * @param {ElementSpec} elSpec * @param {HTMLElement} mountPoint * @param {object} props * @param {HTMLElement | null} element */ function update(elSpec, mountPoint, props, element) { var component = elSpec.component; var reactElement = React.createElement(component, props); ReactDOM.render(reactElement, mountPoint); if (element) { reactShadowDomRetargetEvents(element.shadowRoot); } } /** * Unmounts a component. * @private * * @param {ElementSpec} elSpec * @param {HTMLElement} mountPoint */ function unmount(elSpec, mountPoint) { ReactDOM.unmountComponentAtNode(mountPoint); } var ReactAdapter = /*#__PURE__*/Object.freeze({ mount: mount, update: update, unmount: unmount }); /** * Cache of the strategy determined by `getStrategy()`. * @type {Strategy | null | undefined} */ var cachedStrategy; /** * Detect what API can be used. * * @example * Remount.getStrategy().name */ function getStrategy() { if (cachedStrategy) { return cachedStrategy; } var StrategyUsed = [CustomElementsStrategy, MutationObserverStrategy].find(function (strategy) { return !!strategy.isSupported(); }); if (!StrategyUsed) { /* tslint:disable no-console */ console.warn("Remount: This browser doesn't support the " + 'MutationObserver API or the Custom Elements API. Including ' + 'polyfills might fix this. Remount elements will not work. ' + 'https://github.com/rstacruz/remount'); } cachedStrategy = StrategyUsed; return StrategyUsed; } /** * Registers custom elements and links them to React components. * @param {ElementMap} components * @param {Defaults=} defaults * * @example * define({ 'x-tooltip': Tooltip }) * * @example * define( * { 'x-tooltip': Tooltip }, * { attributes: ['title', 'body'] } * ) */ function define(components, defaults) { var Strategy = getStrategy(); if (!Strategy) { return; } Object.keys(components).forEach(function ( /** @type string */ name$$1) { // Construct the specs for the element. // (eg, { component: Tooltip, attributes: ['title'] }) /** @type ElementSpec */ var elSpec = Object.assign({}, defaults, toElementSpec(components[name$$1])); /** @type Adapter */ var adapter = elSpec.adapter || ReactAdapter; // Define a custom element. Strategy.defineElement(elSpec, name$$1, { onMount: function onMount(element, mountPoint) { var props = getProps(element, elSpec.attributes); if (elSpec.shadow && elSpec.retarget) { adapter.mount(elSpec, mountPoint, props, element); } else { adapter.mount(elSpec, mountPoint, props, null); } }, onUpdate: function onUpdate(element, mountPoint) { var props = getProps(element, elSpec.attributes); adapter.update(elSpec, mountPoint, props, null); }, onUnmount: function onUnmount(element, mountPoint) { adapter.unmount(elSpec, mountPoint); } }); }); } /** * Coerces something into an `ElementSpec` type. * * @param {ElementSpec | Component} thing * @returns {ElementSpec} * @private * * @example * toElementSpec(Tooltip) * // => { component: Tooltip } * * toElementSpec({ component: Tooltip }) * // => { component: Tooltip } */ function toElementSpec(thing) { if (isElementSpec(thing)) { return thing; } return { component: thing }; } /** * Checks if a given `spec` is an ElementSpec. * * @param {any} spec * @returns {spec is ElementSpec} */ function isElementSpec(spec) { return _typeof(spec) === 'object' && spec.component; } /** * Returns properties for a given HTML element. * * @private * @param {HTMLElement} element * @param {string[] | null | undefined} attributes * * @example * getProps(div, ['name']) * // => { name: 'Romeo' } */ function getProps(element, attributes) { var rawJson = element.getAttribute('props-json'); if (rawJson) { return JSON.parse(rawJson); } var names = attributes || []; return names.reduce(function ( /** @type PropertyMap */ result, /** @type string */ attribute) { result[attribute] = element.getAttribute(attribute); return result; }, {}); } exports.getStrategy = getStrategy; exports.define = define; Object.defineProperty(exports, '__esModule', { value: true }); }));