cat-bookmarker/assets/node_modules/remount/dist/remount.js

584 lines
13 KiB
JavaScript
Raw Permalink Normal View History

2024-03-10 18:52:04 +00:00
import { createElement } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import retargetEvents from 'react-shadow-dom-retarget-events';
/* 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
}
const 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;
}
// @ts-check
/**
* The name of this strategy.
* @type string
*/
const 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) {
const { onUpdate, onUnmount, onMount } = events;
inject();
const attributes = elSpec.attributes || [];
class ComponentElement extends HTMLElement {
static get observedAttributes() {
return ['props-json', ...attributes]
}
connectedCallback() {
this._mountPoint = createMountPoint(this, elSpec);
onMount(this, this._mountPoint);
}
disconnectedCallback() {
if (!this._mountPoint) {
return
}
onUnmount(this, this._mountPoint);
}
attributeChangedCallback() {
if (!this._mountPoint) {
return
}
onUpdate(this, this._mountPoint);
}
}
// 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 `<span>` 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) {
const { shadow } = elSpec;
if (shadow && element.attachShadow) {
const 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 (let i = 0, len = list.length; i < len; i++) {
fn(list[i]);
}
}
// @ts-check
/**
* The name of this strategy.
* @type string
*/
const name$1 = 'MutationObserver';
/**
* List of observers tags.
* @type ObserverList
*/
const 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: "${elName}" is not a valid custom element elName`)
}
if (observers[elName]) {
if (elSpec.quiet) {
return
}
throw new Error(`Remount: "${elName}" is already registered`)
}
const observer = new MutationObserver(
/** @type MutationCallback */ mutations => {
each(mutations, (/** @type MutationRecord */ mutation) => {
each(mutation.addedNodes, (/** @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', () => {
const nodes = document.getElementsByTagName(name$1);
each(nodes, (/** @type HTMLElement */ node) =>
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, (/** @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) {
const { onUpdate } = events;
const observer = new MutationObserver(
/** @type MutationCallback */ mutations => {
each(mutations, (/** @type MutationRecord */ mutation) => {
const 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) {
const { onUnmount } = events;
const parent = node.parentNode;
// Not sure when this can happen, but let's add this for type safety
if (!parent) {
return
}
const observer = new MutationObserver(
/** @type MutationCallback */ mutations => {
each(mutations, (/** @type MutationRecord */ mutation) => {
each(mutation.removedNodes, (/** @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
});
// @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) {
const { component } = elSpec;
const reactElement = createElement(component, props);
render(reactElement, mountPoint);
if (element) {
retargetEvents(element.shadowRoot);
}
}
/**
* Unmounts a component.
* @private
*
* @param {ElementSpec} elSpec
* @param {HTMLElement} mountPoint
*/
function unmount(elSpec, mountPoint) {
unmountComponentAtNode(mountPoint);
}
var ReactAdapter = /*#__PURE__*/Object.freeze({
mount: mount,
update: update,
unmount: unmount
});
// @ts-check
/**
* Cache of the strategy determined by `getStrategy()`.
* @type {Strategy | null | undefined}
*/
let cachedStrategy;
/**
* Detect what API can be used.
*
* @example
* Remount.getStrategy().name
*/
function getStrategy() {
if (cachedStrategy) {
return cachedStrategy
}
const StrategyUsed = [CustomElementsStrategy, MutationObserverStrategy].find(
strategy => !!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) {
const Strategy = getStrategy();
if (!Strategy) {
return
}
Object.keys(components).forEach((/** @type string */ name$$1) => {
// Construct the specs for the element.
// (eg, { component: Tooltip, attributes: ['title'] })
/** @type ElementSpec */
const elSpec = Object.assign({}, defaults, toElementSpec(components[name$$1]));
/** @type Adapter */
const adapter = elSpec.adapter || ReactAdapter;
// Define a custom element.
Strategy.defineElement(elSpec, name$$1, {
onMount(element, mountPoint) {
const props = getProps(element, elSpec.attributes);
if (elSpec.shadow && elSpec.retarget) {
adapter.mount(elSpec, mountPoint, props, element);
} else {
adapter.mount(elSpec, mountPoint, props, null);
}
},
onUpdate(element, mountPoint) {
const props = getProps(element, elSpec.attributes);
adapter.update(elSpec, mountPoint, props, null);
},
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) {
const rawJson = element.getAttribute('props-json');
if (rawJson) {
return JSON.parse(rawJson)
}
const names = attributes || [];
return names.reduce((
/** @type PropertyMap */ result,
/** @type string */ attribute
) => {
result[attribute] = element.getAttribute(attribute);
return result
}, {})
}
export { getStrategy, define };