@plone/volto
Version:
Volto
283 lines (244 loc) • 8.49 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import withSideEffect from 'react-side-effect';
import isEqual from 'react-fast-compare';
import {
convertReactPropstoHtmlAttributes,
handleClientStateChange,
mapStateOnServer,
reducePropsToState,
warn,
} from './HelmetUtils.js';
import { TAG_NAMES, VALID_TAG_NAMES } from './HelmetConstants.js';
const Helmet = (Component) =>
class HelmetWrapper extends React.Component {
/**
* @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"}
* @param {Object} bodyAttributes: {"className": "root"}
* @param {String} defaultTitle: "Default Title"
* @param {Boolean} defer: true
* @param {Boolean} encodeSpecialCharacters: true
* @param {Object} htmlAttributes: {"lang": "en", "amp": undefined}
* @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}]
* @param {Array} meta: [{"name": "description", "content": "Test description"}]
* @param {Array} noscript: [{"innerHTML": "<img src='http://mysite.com/js/test.js'"}]
* @param {Function} onChangeClientState: "(newState) => console.log(newState)"
* @param {Array} script: [{"type": "text/javascript", "src": "http://mysite.com/js/test.js"}]
* @param {Array} style: [{"type": "text/css", "cssText": "div { display: block; color: blue; }"}]
* @param {String} title: "Title"
* @param {Object} titleAttributes: {"itemprop": "name"}
* @param {String} titleTemplate: "MySite.com - %s"
*/
static propTypes = {
base: PropTypes.object,
bodyAttributes: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
defaultTitle: PropTypes.string,
defer: PropTypes.bool,
encodeSpecialCharacters: PropTypes.bool,
htmlAttributes: PropTypes.object,
link: PropTypes.arrayOf(PropTypes.object),
meta: PropTypes.arrayOf(PropTypes.object),
noscript: PropTypes.arrayOf(PropTypes.object),
onChangeClientState: PropTypes.func,
script: PropTypes.arrayOf(PropTypes.object),
style: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string,
titleAttributes: PropTypes.object,
titleTemplate: PropTypes.string,
};
static defaultProps = {
defer: true,
encodeSpecialCharacters: true,
};
// Component.peek comes from react-side-effect:
// For testing, you may use a static peek() method available on the returned component.
// It lets you get the current state without resetting the mounted instance stack.
// Don’t use it for anything other than testing.
static peek = Component.peek;
static rewind = () => {
let mappedState = Component.rewind();
if (!mappedState) {
// provide fallback if mappedState is undefined
mappedState = mapStateOnServer({
baseTag: [],
bodyAttributes: {},
encodeSpecialCharacters: true,
htmlAttributes: {},
linkTags: [],
metaTags: [],
noscriptTags: [],
scriptTags: [],
styleTags: [],
title: '',
titleAttributes: {},
});
}
return mappedState;
};
static set canUseDOM(canUseDOM) {
Component.canUseDOM = canUseDOM;
}
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
mapNestedChildrenToProps(child, nestedChildren) {
if (!nestedChildren) {
return null;
}
// eslint-disable-next-line default-case
switch (child.type) {
case TAG_NAMES.SCRIPT:
case TAG_NAMES.NOSCRIPT:
return {
innerHTML: nestedChildren,
};
case TAG_NAMES.STYLE:
return {
cssText: nestedChildren,
};
}
throw new Error(
`<${child.type} /> elements are self-closing and can not contain children. Refer to our API for more information.`,
);
}
flattenArrayTypeChildren({
child,
arrayTypeChildren,
newChildProps,
nestedChildren,
}) {
return {
...arrayTypeChildren,
[child.type]: [
...(arrayTypeChildren[child.type] || []),
{
...newChildProps,
...this.mapNestedChildrenToProps(child, nestedChildren),
},
],
};
}
mapObjectTypeChildren({ child, newProps, newChildProps, nestedChildren }) {
// eslint-disable-next-line default-case
switch (child.type) {
case TAG_NAMES.TITLE:
return {
...newProps,
[child.type]: nestedChildren,
titleAttributes: { ...newChildProps },
};
case TAG_NAMES.BODY:
return {
...newProps,
bodyAttributes: { ...newChildProps },
};
case TAG_NAMES.HTML:
return {
...newProps,
htmlAttributes: { ...newChildProps },
};
}
return {
...newProps,
[child.type]: { ...newChildProps },
};
}
mapArrayTypeChildrenToProps(arrayTypeChildren, newProps) {
let newFlattenedProps = { ...newProps };
Object.keys(arrayTypeChildren).forEach((arrayChildName) => {
newFlattenedProps = {
...newFlattenedProps,
[arrayChildName]: arrayTypeChildren[arrayChildName],
};
});
return newFlattenedProps;
}
warnOnInvalidChildren(child, nestedChildren) {
if (process.env.NODE_ENV !== 'production') {
if (!VALID_TAG_NAMES.some((name) => child.type === name)) {
if (typeof child.type === 'function') {
return warn(
`You may be attempting to nest <Helmet> components within each other, which is not allowed. Refer to our API for more information.`,
);
}
return warn(
`Only elements types ${VALID_TAG_NAMES.join(
', ',
)} are allowed. Helmet does not support rendering <${
child.type
}> elements. Refer to our API for more information.`,
);
}
if (
nestedChildren &&
typeof nestedChildren !== 'string' &&
(!Array.isArray(nestedChildren) ||
nestedChildren.some(
(nestedChild) => typeof nestedChild !== 'string',
))
) {
throw new Error(
`Helmet expects a string as a child of <${child.type}>. Did you forget to wrap your children in braces? ( <${child.type}>{\`\`}</${child.type}> ) Refer to our API for more information.`,
);
}
}
return true;
}
mapChildrenToProps(children, newProps) {
let arrayTypeChildren = {};
React.Children.forEach(children, (child) => {
if (!child || !child.props) {
return;
}
const { children: nestedChildren, ...childProps } = child.props;
const newChildProps = convertReactPropstoHtmlAttributes(childProps);
this.warnOnInvalidChildren(child, nestedChildren);
switch (child.type) {
case TAG_NAMES.LINK:
case TAG_NAMES.META:
case TAG_NAMES.NOSCRIPT:
case TAG_NAMES.SCRIPT:
case TAG_NAMES.STYLE:
arrayTypeChildren = this.flattenArrayTypeChildren({
child,
arrayTypeChildren,
newChildProps,
nestedChildren,
});
break;
default:
newProps = this.mapObjectTypeChildren({
child,
newProps,
newChildProps,
nestedChildren,
});
break;
}
});
newProps = this.mapArrayTypeChildrenToProps(arrayTypeChildren, newProps);
return newProps;
}
render() {
const { children, ...props } = this.props;
let newProps = { ...props };
if (children) {
newProps = this.mapChildrenToProps(children, newProps);
}
return <Component {...newProps} />;
}
};
const NullComponent = () => null;
const HelmetSideEffects = withSideEffect(
reducePropsToState,
handleClientStateChange,
mapStateOnServer,
)(NullComponent);
const HelmetExport = Helmet(HelmetSideEffects);
HelmetExport.renderStatic = HelmetExport.rewind;
export { HelmetExport as Helmet };
export default HelmetExport;