UNPKG

@plone/volto

Version:
283 lines (244 loc) 8.49 kB
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;