UNPKG

preact-helmet

Version:
520 lines (428 loc) 22.7 kB
exports.__esModule = true; var _createClass = 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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _preact = require("preact"); var _preactSideEffect = require("preact-side-effect"); var _preactSideEffect2 = _interopRequireDefault(_preactSideEffect); var _deepEqual = require("deep-equal"); var _deepEqual2 = _interopRequireDefault(_deepEqual); var _objectAssign = require("object-assign"); var _objectAssign2 = _interopRequireDefault(_objectAssign); var _HelmetConstants = require("./HelmetConstants.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var HELMET_ATTRIBUTE = "data-preact-helmet"; var encodeSpecialCharacters = function encodeSpecialCharacters(str) { return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;"); }; var getInnermostProperty = function getInnermostProperty(propsList, property) { for (var i = propsList.length - 1; i >= 0; i--) { var props = propsList[i]; if (props[property]) { return props[property]; } } return null; }; var getTitleFromPropsList = function getTitleFromPropsList(propsList) { var innermostTitle = getInnermostProperty(propsList, "title"); var innermostTemplate = getInnermostProperty(propsList, "titleTemplate"); if (innermostTemplate && innermostTitle) { // use function arg to avoid need to escape $ characters return innermostTemplate.replace(/%s/g, function () { return innermostTitle; }); } var innermostDefaultTitle = getInnermostProperty(propsList, "defaultTitle"); return innermostTitle || innermostDefaultTitle || ""; }; var getOnChangeClientState = function getOnChangeClientState(propsList) { return getInnermostProperty(propsList, "onChangeClientState") || function () {}; }; var getAttributesFromPropsList = function getAttributesFromPropsList(tagType, propsList) { return propsList.filter(function (props) { return typeof props[tagType] !== "undefined"; }).map(function (props) { return props[tagType]; }).reduce(function (tagAttrs, current) { return _extends({}, tagAttrs, current); }, {}); }; var getBaseTagFromPropsList = function getBaseTagFromPropsList(primaryAttributes, propsList) { return propsList.filter(function (props) { return typeof props[_HelmetConstants.TAG_NAMES.BASE] !== "undefined"; }).map(function (props) { return props[_HelmetConstants.TAG_NAMES.BASE]; }).reverse().reduce(function (innermostBaseTag, tag) { if (!innermostBaseTag.length) { var keys = Object.keys(tag); for (var i = 0; i < keys.length; i++) { var attributeKey = keys[i]; var lowerCaseAttributeKey = attributeKey.toLowerCase(); if (primaryAttributes.indexOf(lowerCaseAttributeKey) !== -1 && tag[lowerCaseAttributeKey]) { return innermostBaseTag.concat(tag); } } } return innermostBaseTag; }, []); }; var getTagsFromPropsList = function getTagsFromPropsList(tagName, primaryAttributes, propsList) { // Calculate list of tags, giving priority innermost component (end of the propslist) var approvedSeenTags = {}; return propsList.filter(function (props) { return typeof props[tagName] !== "undefined"; }).map(function (props) { return props[tagName]; }).reverse().reduce(function (approvedTags, instanceTags) { var instanceSeenTags = {}; instanceTags.filter(function (tag) { var primaryAttributeKey = void 0; var keys = Object.keys(tag); for (var i = 0; i < keys.length; i++) { var attributeKey = keys[i]; var lowerCaseAttributeKey = attributeKey.toLowerCase(); // Special rule with link tags, since rel and href are both primary tags, rel takes priority if (primaryAttributes.indexOf(lowerCaseAttributeKey) !== -1 && !(primaryAttributeKey === _HelmetConstants.TAG_PROPERTIES.REL && tag[primaryAttributeKey].toLowerCase() === "canonical") && !(lowerCaseAttributeKey === _HelmetConstants.TAG_PROPERTIES.REL && tag[lowerCaseAttributeKey].toLowerCase() === "stylesheet")) { primaryAttributeKey = lowerCaseAttributeKey; } // Special case for innerHTML which doesn't work lowercased if (primaryAttributes.indexOf(attributeKey) !== -1 && (attributeKey === _HelmetConstants.TAG_PROPERTIES.INNER_HTML || attributeKey === _HelmetConstants.TAG_PROPERTIES.CSS_TEXT || attributeKey === _HelmetConstants.TAG_PROPERTIES.ITEM_PROP)) { primaryAttributeKey = attributeKey; } } if (!primaryAttributeKey || !tag[primaryAttributeKey]) { return false; } var value = tag[primaryAttributeKey].toLowerCase(); if (!approvedSeenTags[primaryAttributeKey]) { approvedSeenTags[primaryAttributeKey] = {}; } if (!instanceSeenTags[primaryAttributeKey]) { instanceSeenTags[primaryAttributeKey] = {}; } if (!approvedSeenTags[primaryAttributeKey][value]) { instanceSeenTags[primaryAttributeKey][value] = true; return true; } return false; }).reverse().forEach(function (tag) { return approvedTags.push(tag); }); // Update seen tags with tags from this instance var keys = Object.keys(instanceSeenTags); for (var i = 0; i < keys.length; i++) { var attributeKey = keys[i]; var tagUnion = (0, _objectAssign2.default)({}, approvedSeenTags[attributeKey], instanceSeenTags[attributeKey]); approvedSeenTags[attributeKey] = tagUnion; } return approvedTags; }, []).reverse(); }; var updateTitle = function updateTitle(title, attributes) { document.title = title || document.title; updateAttributes(_HelmetConstants.TAG_NAMES.TITLE, attributes); }; var updateAttributes = function updateAttributes(tagName, attributes) { var htmlTag = document.getElementsByTagName(tagName)[0]; var helmetAttributeString = htmlTag.getAttribute(HELMET_ATTRIBUTE); var helmetAttributes = helmetAttributeString ? helmetAttributeString.split(",") : []; var attributesToRemove = [].concat(helmetAttributes); var attributeKeys = Object.keys(attributes); for (var i = 0; i < attributeKeys.length; i++) { var attribute = attributeKeys[i]; var value = attributes[attribute] || ""; htmlTag.setAttribute(attribute, value); if (helmetAttributes.indexOf(attribute) === -1) { helmetAttributes.push(attribute); } var indexToSave = attributesToRemove.indexOf(attribute); if (indexToSave !== -1) { attributesToRemove.splice(indexToSave, 1); } } for (var _i = attributesToRemove.length - 1; _i >= 0; _i--) { htmlTag.removeAttribute(attributesToRemove[_i]); } if (helmetAttributes.length === attributesToRemove.length) { htmlTag.removeAttribute(HELMET_ATTRIBUTE); } else { htmlTag.setAttribute(HELMET_ATTRIBUTE, helmetAttributes.join(",")); } }; var updateTags = function updateTags(type, tags) { var headElement = document.head || document.querySelector("head"); var tagNodes = headElement.querySelectorAll(type + "[" + HELMET_ATTRIBUTE + "]"); var oldTags = Array.prototype.slice.call(tagNodes); var newTags = []; var indexToDelete = void 0; if (tags && tags.length) { tags.forEach(function (tag) { var newElement = document.createElement(type); for (var attribute in tag) { if (tag.hasOwnProperty(attribute)) { if (attribute === "innerHTML") { newElement.innerHTML = tag.innerHTML; } else if (attribute === "cssText") { if (newElement.styleSheet) { newElement.styleSheet.cssText = tag.cssText; } else { newElement.appendChild(document.createTextNode(tag.cssText)); } } else { var value = typeof tag[attribute] === "undefined" ? "" : tag[attribute]; newElement.setAttribute(attribute, value); } } } newElement.setAttribute(HELMET_ATTRIBUTE, "true"); // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. if (oldTags.some(function (existingTag, index) { indexToDelete = index; return newElement.isEqualNode(existingTag); })) { oldTags.splice(indexToDelete, 1); } else { newTags.push(newElement); } }); } oldTags.forEach(function (tag) { return tag.parentNode.removeChild(tag); }); newTags.forEach(function (tag) { return headElement.appendChild(tag); }); return { oldTags: oldTags, newTags: newTags }; }; var generateHtmlAttributesAsString = function generateHtmlAttributesAsString(attributes) { return Object.keys(attributes).reduce(function (str, key) { var attr = typeof attributes[key] !== "undefined" ? key + "=\"" + attributes[key] + "\"" : "" + key; return str ? str + " " + attr : attr; }, ""); }; var generateTitleAsString = function generateTitleAsString(type, title, attributes) { var attributeString = generateHtmlAttributesAsString(attributes); return attributeString ? "<" + type + " " + HELMET_ATTRIBUTE + " " + attributeString + ">" + encodeSpecialCharacters(title) + "</" + type + ">" : "<" + type + " " + HELMET_ATTRIBUTE + ">" + encodeSpecialCharacters(title) + "</" + type + ">"; }; var generateTagsAsString = function generateTagsAsString(type, tags) { return tags.reduce(function (str, tag) { var attributeHtml = Object.keys(tag).filter(function (attribute) { return !(attribute === "innerHTML" || attribute === "cssText"); }).reduce(function (string, attribute) { var attr = typeof tag[attribute] === "undefined" ? attribute : attribute + "=\"" + encodeSpecialCharacters(tag[attribute]) + "\""; return string ? string + " " + attr : attr; }, ""); var tagContent = tag.innerHTML || tag.cssText || ""; var isSelfClosing = [_HelmetConstants.TAG_NAMES.NOSCRIPT, _HelmetConstants.TAG_NAMES.SCRIPT, _HelmetConstants.TAG_NAMES.STYLE].indexOf(type) === -1; return str + "<" + type + " " + HELMET_ATTRIBUTE + " " + attributeHtml + (isSelfClosing ? ">" : ">" + tagContent + "</" + type + ">"); }, ""); }; var generateTitleAsPreactComponent = function generateTitleAsPreactComponent(type, title, attributes) { // assigning into an array to define toString function on it var initProps = _defineProperty({ key: title }, HELMET_ATTRIBUTE, true); var props = Object.keys(attributes).reduce(function (obj, key) { obj[key] = attributes[key]; return obj; }, initProps); return [(0, _preact.h)(_HelmetConstants.TAG_NAMES.TITLE, props, title)]; }; var generateTagsAsPreactComponent = function generateTagsAsPreactComponent(type, tags) { return tags.map(function (tag, i) { var mappedTag = _defineProperty({ key: i }, HELMET_ATTRIBUTE, true); Object.keys(tag).forEach(function (attribute) { var mappedAttribute = attribute; if (mappedAttribute === "innerHTML" || mappedAttribute === "cssText") { var content = tag.innerHTML || tag.cssText; mappedTag.dangerouslySetInnerHTML = { __html: content }; } else { mappedTag[mappedAttribute] = tag[attribute]; } }); return (0, _preact.h)(type, mappedTag); }); }; var getMethodsForTag = function getMethodsForTag(type, tags) { switch (type) { case _HelmetConstants.TAG_NAMES.TITLE: return { toComponent: function toComponent() { return generateTitleAsPreactComponent(type, tags.title, tags.titleAttributes); }, toString: function toString() { return generateTitleAsString(type, tags.title, tags.titleAttributes); } }; case _HelmetConstants.TAG_NAMES.HTML: return { toComponent: function toComponent() { return tags; }, toString: function toString() { return generateHtmlAttributesAsString(tags); } }; default: return { toComponent: function toComponent() { return generateTagsAsPreactComponent(type, tags); }, toString: function toString() { return generateTagsAsString(type, tags); } }; } }; var mapStateOnServer = function mapStateOnServer(_ref) { var htmlAttributes = _ref.htmlAttributes, title = _ref.title, titleAttributes = _ref.titleAttributes, baseTag = _ref.baseTag, metaTags = _ref.metaTags, linkTags = _ref.linkTags, scriptTags = _ref.scriptTags, noscriptTags = _ref.noscriptTags, styleTags = _ref.styleTags; return { htmlAttributes: getMethodsForTag(_HelmetConstants.TAG_NAMES.HTML, htmlAttributes), title: getMethodsForTag(_HelmetConstants.TAG_NAMES.TITLE, { title: title, titleAttributes: titleAttributes }), base: getMethodsForTag(_HelmetConstants.TAG_NAMES.BASE, baseTag), meta: getMethodsForTag(_HelmetConstants.TAG_NAMES.META, metaTags), link: getMethodsForTag(_HelmetConstants.TAG_NAMES.LINK, linkTags), script: getMethodsForTag(_HelmetConstants.TAG_NAMES.SCRIPT, scriptTags), noscript: getMethodsForTag(_HelmetConstants.TAG_NAMES.NOSCRIPT, noscriptTags), style: getMethodsForTag(_HelmetConstants.TAG_NAMES.STYLE, styleTags) }; }; /** * @param {Object} htmlAttributes: {"lang": "en", "amp": undefined} * @param {String} title: "Title" * @param {String} defaultTitle: "Default Title" * @param {String} titleTemplate: "MySite.com - %s" * @param {Object} titleAttributes: {"itemprop": "name"} * @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"} * @param {Array} meta: [{"name": "description", "content": "Test description"}] * @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}] * @param {Array} script: [{"type": "text/javascript", "src": "http://mysite.com/js/test.js"}] * @param {Array} noscript: [{"innerHTML": "<img src='http://mysite.com/js/test.js'"}] * @param {Array} style: [{"type": "text/css", "cssText": "div{ display: block; color: blue; }"}] * @param {Function} onChangeClientState: "(newState) => console.log(newState)" */ var Helmet = function Helmet(WrappedComponent) { var _class, _temp; return _temp = _class = function (_Component) { _inherits(HelmetWrapper, _Component); function HelmetWrapper() { _classCallCheck(this, HelmetWrapper); return _possibleConstructorReturn(this, (HelmetWrapper.__proto__ || Object.getPrototypeOf(HelmetWrapper)).apply(this, arguments)); } _createClass(HelmetWrapper, [{ key: "shouldComponentUpdate", value: function shouldComponentUpdate(nextProps) { var props = _extends({}, nextProps); if (!props.children || !props.children.length) { delete props.children; } return !(0, _deepEqual2.default)(this.props, props); } }, { key: "render", value: function render() { return (0, _preact.h)(WrappedComponent, this.props); } }], [{ key: "canUseDOM", // WrappedComponent.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. set: function set(canUseDOM) { WrappedComponent.canUseDOM = canUseDOM; } }]); return HelmetWrapper; }(_preact.Component), _class.peek = WrappedComponent.peek, _class.rewind = function () { var mappedState = WrappedComponent.rewind(); if (!mappedState) { // provide fallback if mappedState is undefined mappedState = mapStateOnServer({ htmlAttributes: {}, title: "", titleAttributes: {}, baseTag: [], metaTags: [], linkTags: [], scriptTags: [], noscriptTags: [], styleTags: [] }); } return mappedState; }, _temp; }; var reducePropsToState = function reducePropsToState(propsList) { return { htmlAttributes: getAttributesFromPropsList(_HelmetConstants.TAG_NAMES.HTML, propsList), title: getTitleFromPropsList(propsList), titleAttributes: getAttributesFromPropsList("titleAttributes", propsList), baseTag: getBaseTagFromPropsList([_HelmetConstants.TAG_PROPERTIES.HREF], propsList), metaTags: getTagsFromPropsList(_HelmetConstants.TAG_NAMES.META, [_HelmetConstants.TAG_PROPERTIES.NAME, _HelmetConstants.TAG_PROPERTIES.CHARSET, _HelmetConstants.TAG_PROPERTIES.HTTPEQUIV, _HelmetConstants.TAG_PROPERTIES.PROPERTY, _HelmetConstants.TAG_PROPERTIES.ITEM_PROP], propsList), linkTags: getTagsFromPropsList(_HelmetConstants.TAG_NAMES.LINK, [_HelmetConstants.TAG_PROPERTIES.REL, _HelmetConstants.TAG_PROPERTIES.HREF], propsList), scriptTags: getTagsFromPropsList(_HelmetConstants.TAG_NAMES.SCRIPT, [_HelmetConstants.TAG_PROPERTIES.SRC, _HelmetConstants.TAG_PROPERTIES.INNER_HTML], propsList), noscriptTags: getTagsFromPropsList(_HelmetConstants.TAG_NAMES.NOSCRIPT, [_HelmetConstants.TAG_PROPERTIES.INNER_HTML], propsList), styleTags: getTagsFromPropsList(_HelmetConstants.TAG_NAMES.STYLE, [_HelmetConstants.TAG_PROPERTIES.CSS_TEXT], propsList), onChangeClientState: getOnChangeClientState(propsList) }; }; var handleClientStateChange = function handleClientStateChange(newState) { var htmlAttributes = newState.htmlAttributes, title = newState.title, titleAttributes = newState.titleAttributes, baseTag = newState.baseTag, metaTags = newState.metaTags, linkTags = newState.linkTags, scriptTags = newState.scriptTags, noscriptTags = newState.noscriptTags, styleTags = newState.styleTags, onChangeClientState = newState.onChangeClientState; updateAttributes("html", htmlAttributes); updateTitle(title, titleAttributes); var tagUpdates = { baseTag: updateTags(_HelmetConstants.TAG_NAMES.BASE, baseTag), metaTags: updateTags(_HelmetConstants.TAG_NAMES.META, metaTags), linkTags: updateTags(_HelmetConstants.TAG_NAMES.LINK, linkTags), scriptTags: updateTags(_HelmetConstants.TAG_NAMES.SCRIPT, scriptTags), noscriptTags: updateTags(_HelmetConstants.TAG_NAMES.NOSCRIPT, noscriptTags), styleTags: updateTags(_HelmetConstants.TAG_NAMES.STYLE, styleTags) }; var addedTags = {}; var removedTags = {}; Object.keys(tagUpdates).forEach(function (tagType) { var _tagUpdates$tagType = tagUpdates[tagType], newTags = _tagUpdates$tagType.newTags, oldTags = _tagUpdates$tagType.oldTags; if (newTags.length) { addedTags[tagType] = newTags; } if (oldTags.length) { removedTags[tagType] = tagUpdates[tagType].oldTags; } }); onChangeClientState(newState, addedTags, removedTags); }; var NullComponent = function NullComponent() { return null; }; var HelmetSideEffects = (0, _preactSideEffect2.default)(reducePropsToState, handleClientStateChange, mapStateOnServer)(NullComponent); exports.default = Helmet(HelmetSideEffects); module.exports = exports["default"];