preact-helmet
Version:
A document head manager for Preact
520 lines (428 loc) • 22.7 kB
JavaScript
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
};
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"];