From 4ca305011244ce620e0bf61255bd6c99610c2bc0 Mon Sep 17 00:00:00 2001 From: pimlie Date: Fri, 21 Jun 2019 22:12:22 +0200 Subject: [PATCH] chore: add dist folder to repo --- .gitignore | 1 - dist/vue-meta.common.js | 1296 ++++++++++++++++++++++++++++++ dist/vue-meta.esm.browser.js | 1042 ++++++++++++++++++++++++ dist/vue-meta.esm.browser.min.js | 1 + dist/vue-meta.esm.js | 1292 +++++++++++++++++++++++++++++ dist/vue-meta.js | 1243 ++++++++++++++++++++++++++++ dist/vue-meta.min.js | 1 + package.json | 2 +- 8 files changed, 4876 insertions(+), 2 deletions(-) create mode 100644 dist/vue-meta.common.js create mode 100644 dist/vue-meta.esm.browser.js create mode 100644 dist/vue-meta.esm.browser.min.js create mode 100644 dist/vue-meta.esm.js create mode 100644 dist/vue-meta.js create mode 100644 dist/vue-meta.min.js diff --git a/.gitignore b/.gitignore index ccf3301..8a3d346 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ package-lock.json # built code lib -dist es .vue-meta diff --git a/dist/vue-meta.common.js b/dist/vue-meta.common.js new file mode 100644 index 0000000..1a464f6 --- /dev/null +++ b/dist/vue-meta.common.js @@ -0,0 +1,1296 @@ +/** + * vue-meta v2.0.3 + * (c) 2019 + * - Declan de Wet + * - Sébastien Chopin (@Atinux) + * - All the amazing contributors + * @license MIT + */ + +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var deepmerge = _interopDefault(require('deepmerge')); + +var version = "2.0.3"; + +// store an id to keep track of DOM updates +var batchId = null; + +function triggerUpdate(vm, hookName) { + // if an update was triggered during initialization or when an update was triggered by the + // metaInfo watcher, set initialized to null + // then we keep falsy value but know we need to run a triggerUpdate after initialization + if (!vm.$root._vueMeta.initialized && (vm.$root._vueMeta.initializing || hookName === 'watcher')) { + vm.$root._vueMeta.initialized = null; + } + + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { + // batch potential DOM updates to prevent extraneous re-rendering + batchUpdate(function () { return vm.$meta().refresh(); }); + } +} + +/** + * Performs a batched update. + * + * @param {(null|Number)} id - the ID of this update + * @param {Function} callback - the update to perform + * @return {Number} id - a new ID + */ +function batchUpdate(callback, timeout) { + if ( timeout === void 0 ) timeout = 10; + + clearTimeout(batchId); + + batchId = setTimeout(function () { + callback(); + }, timeout); + + return batchId +} + +/** + * checks if passed argument is an array + * @param {any} arg - the object to check + * @return {Boolean} - true if `arg` is an array + */ +function isArray(arg) { + return Array.isArray(arg) +} + +function isUndefined(arg) { + return typeof arg === 'undefined' +} + +function isObject(arg) { + return typeof arg === 'object' +} + +function isFunction(arg) { + return typeof arg === 'function' +} + +function isString(arg) { + return typeof arg === 'string' +} + +function ensureIsArray(arg, key) { + if (!key || !isObject(arg)) { + return isArray(arg) ? arg : [] + } + + if (!isArray(arg[key])) { + arg[key] = []; + } + return arg +} + +function ensuredPush(object, key, el) { + ensureIsArray(object, key); + + object[key].push(el); +} + +// Vue $root instance has a _vueMeta object property, otherwise its a boolean true +function hasMetaInfo(vm) { + if ( vm === void 0 ) vm = this; + + return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) +} + +// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has +function inMetaInfoBranch(vm) { + if ( vm === void 0 ) vm = this; + + return vm && !isUndefined(vm._vueMeta) +} + +function addNavGuards(vm) { + // return when nav guards already added or no router exists + if (vm.$root._vueMeta.navGuards || !vm.$root.$router) { + /* istanbul ignore next */ + return + } + + vm.$root._vueMeta.navGuards = true; + + var $router = vm.$root.$router; + var $meta = vm.$root.$meta(); + + $router.beforeEach(function (to, from, next) { + $meta.pause(); + next(); + }); + + $router.afterEach(function () { + var ref = $meta.resume(); + var metaInfo = ref.metaInfo; + if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) { + metaInfo.afterNavigation(metaInfo); + } + }); +} + +var appId = 1; + +function createMixin(Vue, options) { + // for which Vue lifecycle hooks should the metaInfo be refreshed + var updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + + // watch for client side component updates + return { + beforeCreate: function beforeCreate() { + var this$1 = this; + + Object.defineProperty(this, '_hasMetaInfo', { + configurable: true, + get: function get() { + // Show deprecation warning once when devtools enabled + if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) { + console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); // eslint-disable-line no-console + this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; + } + return hasMetaInfo(this) + } + }); + + // Add a marker to know if it uses metaInfo + // _vnode is used to know that it's attached to a real component + // useful if we use some mixin to add some meta tags (like nuxt-i18n) + if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) { + if (!this.$root._vueMeta) { + this.$root._vueMeta = { appId: appId }; + appId++; + } + + // to speed up updates we keep track of branches which have a component with vue-meta info defined + // if _vueMeta = true it has info, if _vueMeta = false a child has info + if (!this._vueMeta) { + this._vueMeta = true; + + var p = this.$parent; + while (p && p !== this.$root) { + if (isUndefined(p._vueMeta)) { + p._vueMeta = false; + } + p = p.$parent; + } + } + + // coerce function-style metaInfo to a computed prop so we can observe + // it on creation + if (isFunction(this.$options[options.keyName])) { + if (!this.$options.computed) { + this.$options.computed = {}; + } + this.$options.computed.$metaInfo = this.$options[options.keyName]; + + if (!this.$isServer) { + // if computed $metaInfo exists, watch it for updates & trigger a refresh + // when it changes (i.e. automatically handle async actions that affect metaInfo) + // credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux) + ensuredPush(this.$options, 'created', function () { + this$1.$watch('$metaInfo', function () { + triggerUpdate(this, 'watcher'); + }); + }); + } + } + + // force an initial refresh on page load and prevent other lifecycleHooks + // to triggerUpdate until this initial refresh is finished + // this is to make sure that when a page is opened in an inactive tab which + // has throttled rAF/timers we still immediately set the page title + if (isUndefined(this.$root._vueMeta.initialized)) { + this.$root._vueMeta.initialized = this.$isServer; + + if (!this.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'beforeMount', function () { + // if this Vue-app was server rendered, set the appId to 'ssr' + // only one SSR app per page is supported + if (this$1.$root.$el && this$1.$root.$el.hasAttribute('data-server-rendered')) { + this$1.$root._vueMeta.appId = 'ssr'; + } + }); + + // we use the mounted hook here as on page load + ensuredPush(this.$options, 'mounted', function () { + if (!this$1.$root._vueMeta.initialized) { + // used in triggerUpdate to check if a change was triggered + // during initialization + this$1.$root._vueMeta.initializing = true; + + // refresh meta in nextTick so all child components have loaded + this$1.$nextTick(function () { + var this$1 = this; + + var ref = this.$root.$meta().refresh(); + var tags = ref.tags; + var metaInfo = ref.metaInfo; + + // After ssr hydration (identifier by tags === false) check + // if initialized was set to null in triggerUpdate. That'd mean + // that during initilazation changes where triggered which need + // to be applied OR a metaInfo watcher was triggered before the + // current hook was called + // (during initialization all changes are blocked) + if (tags === false && this.$root._vueMeta.initialized === null) { + this.$nextTick(function () { return triggerUpdate(this$1, 'initializing'); }); + } + + this.$root._vueMeta.initialized = true; + delete this.$root._vueMeta.initializing; + + // add the navigation guards if they havent been added yet + // they are needed for the afterNavigation callback + if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) { + addNavGuards(this); + } + }); + } + }); + + // add the navigation guards if requested + if (options.refreshOnceOnNavigation) { + addNavGuards(this); + } + } + } + + // do not trigger refresh on the server side + if (!this.$isServer) { + // no need to add this hooks on server side + updateOnLifecycleHook.forEach(function (lifecycleHook) { + ensuredPush(this$1.$options, lifecycleHook, function () { return triggerUpdate(this$1, lifecycleHook); }); + }); + + // re-render meta data when returning from a child component to parent + ensuredPush(this.$options, 'destroyed', function () { + // Wait that element is hidden before refreshing meta tags (to support animations) + var interval = setInterval(function () { + if (this$1.$el && this$1.$el.offsetParent !== null) { + /* istanbul ignore next line */ + return + } + + clearInterval(interval); + + if (!this$1.$parent) { + /* istanbul ignore next line */ + return + } + + triggerUpdate(this$1, 'destroyed'); + }, 50); + }); + } + } + } + } +} + +/** + * These are constant variables used throughout the application. + */ + +// set some sane defaults +var defaultInfo = { + title: '', + titleChunk: '', + titleTemplate: '%s', + htmlAttrs: {}, + bodyAttrs: {}, + headAttrs: {}, + base: [], + link: [], + meta: [], + style: [], + script: [], + noscript: [], + __dangerouslyDisableSanitizers: [], + __dangerouslyDisableSanitizersByTagID: {} +}; + +// This is the name of the component option that contains all the information that +// gets converted to the various meta tags & attributes for the page. +var keyName = 'metaInfo'; + +// This is the attribute vue-meta arguments on elements to know which it should +// manage and which it should ignore. +var attribute = 'data-vue-meta'; + +// This is the attribute that goes on the `html` tag to inform `vue-meta` +// that the server has already generated the meta tags for the initial render. +var ssrAttribute = 'data-vue-meta-server-rendered'; + +// This is the property that tells vue-meta to overwrite (instead of append) +// an item in a tag list. For example, if you have two `meta` tag list items +// that both have `vmid` of "description", then vue-meta will overwrite the +// shallowest one with the deepest one. +var tagIDKeyName = 'vmid'; + +// This is the key name for possible meta templates +var metaTemplateKeyName = 'template'; + +// This is the key name for the content-holding property +var contentKeyName = 'content'; + +var defaultOptions = { + keyName: keyName, + attribute: attribute, + ssrAttribute: ssrAttribute, + tagIDKeyName: tagIDKeyName, + contentKeyName: contentKeyName, + metaTemplateKeyName: metaTemplateKeyName +}; + +// List of metaInfo property keys which are configuration options (and dont generate html) +var metaInfoOptionKeys = [ + 'titleChunk', + 'titleTemplate', + 'changed', + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' +]; + +// The metaInfo property keys which are used to disable escaping +var disableOptionKeys = [ + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' +]; + +// List of metaInfo property keys which only generates attributes and no tags +var metaInfoAttributeKeys = [ + 'htmlAttrs', + 'headAttrs', + 'bodyAttrs' +]; + +// HTML elements which dont have a head tag (shortened to our needs) +// see: https://www.w3.org/TR/html52/document-metadata.html +var tagsWithoutEndTag = ['base', 'meta', 'link']; + +// HTML elements which can have inner content (shortened to our needs) +var tagsWithInnerContent = ['noscript', 'script', 'style']; + +// Attributes which are inserted as childNodes instead of HTMLAttribute +var tagAttributeAsInnerContent = ['innerHTML', 'cssText']; + +// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202 +var booleanHtmlAttributes = [ + 'allowfullscreen', + 'amp', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'compact', + 'controls', + 'declare', + 'default', + 'defaultchecked', + 'defaultmuted', + 'defaultselected', + 'defer', + 'disabled', + 'enabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'inert', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nohref', + 'noresize', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'pauseonexit', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'sortable', + 'truespeed', + 'typemustmatch', + 'visible' +]; + +function setOptions(options) { + // combine options + options = isObject(options) ? options : {}; + + for (var key in defaultOptions) { + if (!options[key]) { + options[key] = defaultOptions[key]; + } + } + + return options +} + +function getOptions(options) { + var optionsCopy = {}; + for (var key in options) { + optionsCopy[key] = options[key]; + } + return optionsCopy +} + +function pause(refresh) { + if ( refresh === void 0 ) refresh = true; + + this.$root._vueMeta.paused = true; + + return function () { return resume(refresh); } +} + +function resume(refresh) { + if ( refresh === void 0 ) refresh = true; + + this.$root._vueMeta.paused = false; + + if (refresh) { + return this.$root.$meta().refresh() + } +} + +function applyTemplate(ref, headObject, template, chunk) { + var component = ref.component; + var metaTemplateKeyName = ref.metaTemplateKeyName; + var contentKeyName = ref.contentKeyName; + + if (isUndefined(template)) { + template = headObject[metaTemplateKeyName]; + delete headObject[metaTemplateKeyName]; + } + + // return early if no template defined + if (!template) { + return false + } + + if (isUndefined(chunk)) { + chunk = headObject[contentKeyName]; + } + + headObject[contentKeyName] = isFunction(template) + ? template.call(component, chunk) + : template.replace(/%s/g, chunk); + + return true +} + +/* + * To reduce build size, this file provides simple polyfills without + * overly excessive type checking and without modifying + * the global Array.prototype + * The polyfills are automatically removed in the commonjs build + * Also, only files in client/ & shared/ should use these functions + * files in server/ still use normal js function + */ + +function findIndex(array, predicate) { + var arguments$1 = arguments; + + if (!Array.prototype.findIndex) { + // idx needs to be a Number, for..in returns string + for (var idx = 0; idx < array.length; idx++) { + if (predicate.call(arguments$1[2], array[idx], idx, array)) { + return idx + } + } + return -1 + } + return array.findIndex(predicate, arguments[2]) +} + +function toArray(arg) { + if (!Array.from) { + return Array.prototype.slice.call(arg) + } + return Array.from(arg) +} + +function includes(array, value) { + if (!Array.prototype.includes) { + for (var idx in array) { + if (array[idx] === value) { + return true + } + } + + return false + } + return array.includes(value) +} + +var serverSequences = [ + [/&/g, '&'], + [//g, '>'], + [/"/g, '"'], + [/'/g, '''] +]; + +var clientSequences = [ + [/&/g, '\u0026'], + [//g, '\u003e'], + [/"/g, '\u0022'], + [/'/g, '\u0027'] +]; + +// sanitizes potentially dangerous characters +function escape(info, options, escapeOptions) { + var tagIDKeyName = options.tagIDKeyName; + var doEscape = escapeOptions.doEscape; if ( doEscape === void 0 ) doEscape = function (v) { return v; }; + var escaped = {}; + + for (var key in info) { + var value = info[key]; + + // no need to escape configuration options + if (includes(metaInfoOptionKeys, key)) { + escaped[key] = value; + continue + } + + var disableKey = disableOptionKeys[0]; + if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) { + // this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers + escaped[key] = value; + continue + } + + var tagId = info[tagIDKeyName]; + if (tagId) { + disableKey = disableOptionKeys[1]; + + // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped + if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) { + escaped[key] = value; + continue + } + } + + if (isString(value)) { + escaped[key] = doEscape(value); + } else if (isArray(value)) { + escaped[key] = value.map(function (v) { + return isObject(v) + ? escape(v, options, escapeOptions) + : doEscape(v) + }); + } else if (isObject(value)) { + escaped[key] = escape(value, options, escapeOptions); + } else { + escaped[key] = value; + } + } + + return escaped +} + +function arrayMerge(ref, target, source) { + var component = ref.component; + var tagIDKeyName = ref.tagIDKeyName; + var metaTemplateKeyName = ref.metaTemplateKeyName; + var contentKeyName = ref.contentKeyName; + + // we concat the arrays without merging objects contained in, + // but we check for a `vmid` property on each object in the array + // using an O(1) lookup associative array exploit + var destination = []; + + target.forEach(function (targetItem, targetIndex) { + // no tagID so no need to check for duplicity + if (!targetItem[tagIDKeyName]) { + destination.push(targetItem); + return + } + + var sourceIndex = findIndex(source, function (item) { return item[tagIDKeyName] === targetItem[tagIDKeyName]; }); + var sourceItem = source[sourceIndex]; + + // source doesnt contain any duplicate vmid's, we can keep targetItem + if (sourceIndex === -1) { + destination.push(targetItem); + return + } + + // when sourceItem explictly defines contentKeyName or innerHTML as undefined, its + // an indication that we need to skip the default behaviour or child has preference over parent + // which means we keep the targetItem and ignore/remove the sourceItem + if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) || + (sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) { + destination.push(targetItem); + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem + + // if source specifies null as content then ignore both the target as the source + if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) { + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // now we only need to check if the target has a template to combine it with the source + var targetTemplate = targetItem[metaTemplateKeyName]; + if (!targetTemplate) { + return + } + + var sourceTemplate = sourceItem[metaTemplateKeyName]; + + if (!sourceTemplate) { + // use parent template and child content + applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, targetTemplate); + } else if (!sourceItem[contentKeyName]) { + // use child template and parent content + applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + } + }); + + return destination.concat(source) +} + +function merge(target, source, options) { + if ( options === void 0 ) options = {}; + + // remove properties explicitly set to false so child components can + // optionally _not_ overwrite the parents content + // (for array properties this is checked in arrayMerge) + if (source.hasOwnProperty('title') && source.title === undefined) { + delete source.title; + } + + metaInfoAttributeKeys.forEach(function (attrKey) { + if (!source[attrKey]) { + return + } + + for (var key in source[attrKey]) { + if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { + delete source[attrKey][key]; + } + } + }); + + return deepmerge(target, source, { + arrayMerge: function (t, s) { return arrayMerge(options, t, s); } + }) +} + +/** + * Returns the `opts.option` $option value of the given `opts.component`. + * If methods are encountered, they will be bound to the component context. + * If `opts.deep` is true, will recursively merge all child component + * `opts.option` $option values into the returned result. + * + * @param {Object} opts - options + * @param {Object} opts.component - Vue component to fetch option data from + * @param {Boolean} opts.deep - look for data in child components as well? + * @param {Function} opts.arrayMerge - how should arrays be merged? + * @param {String} opts.keyName - the name of the option to look for + * @param {Object} [result={}] - result so far + * @return {Object} result - final aggregated result + */ +function getComponentOption(options, component, result) { + if ( options === void 0 ) options = {}; + if ( result === void 0 ) result = {}; + + var keyName = options.keyName; + var metaTemplateKeyName = options.metaTemplateKeyName; + var tagIDKeyName = options.tagIDKeyName; + var $options = component.$options; + var $children = component.$children; + + if (component._inactive) { + return result + } + + // only collect option data if it exists + if ($options[keyName]) { + var data = $options[keyName]; + + // if option is a function, replace it with it's result + if (isFunction(data)) { + data = data.call(component); + } + + // ignore data if its not an object, then we keep our previous result + if (!isObject(data)) { + return result + } + + // merge with existing options + result = merge(result, data, options); + } + + // collect & aggregate child options if deep = true + if ($children.length) { + $children.forEach(function (childComponent) { + // check if the childComponent is in a branch + // return otherwise so we dont walk all component branches unnecessarily + if (!inMetaInfoBranch(childComponent)) { + return + } + + result = getComponentOption(options, childComponent, result); + }); + } + + if (metaTemplateKeyName && result.meta) { + // apply templates if needed + result.meta.forEach(function (metaObject) { return applyTemplate(options, metaObject); }); + + // remove meta items with duplicate vmid's + result.meta = result.meta.filter(function (metaItem, index, arr) { + return ( + // keep meta item if it doesnt has a vmid + !metaItem.hasOwnProperty(tagIDKeyName) || + // or if it's the first item in the array with this vmid + index === findIndex(arr, function (item) { return item[tagIDKeyName] === metaItem[tagIDKeyName]; }) + ) + }); + } + + return result +} + +/** + * Returns the correct meta info for the given component + * (child components will overwrite parent meta info) + * + * @param {Object} component - the Vue instance to get meta info from + * @return {Object} - returned meta info + */ +function getMetaInfo(options, component, escapeSequences) { + if ( options === void 0 ) options = {}; + if ( escapeSequences === void 0 ) escapeSequences = []; + + // collect & aggregate all metaInfo $options + var info = getComponentOption(options, component, defaultInfo); + + // Remove all "template" tags from meta + + // backup the title chunk in case user wants access to it + if (info.title) { + info.titleChunk = info.title; + } + + // replace title with populated template + if (info.titleTemplate && info.titleTemplate !== '%s') { + applyTemplate({ component: component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || ''); + } + + // convert base tag to an array so it can be handled the same way + // as the other tags + if (info.base) { + info.base = Object.keys(info.base).length ? [info.base] : []; + } + + var escapeOptions = { + doEscape: function (value) { return escapeSequences.reduce(function (val, ref) { + var v = ref[0]; + var r = ref[1]; + + return val.replace(v, r); + }, value); } + }; + + disableOptionKeys.forEach(function (disableKey, index) { + if (index === 0) { + ensureIsArray(info, disableKey); + } else if (index === 1) { + for (var key in info[disableKey]) { + ensureIsArray(info[disableKey], key); + } + } + + escapeOptions[disableKey] = info[disableKey]; + }); + + // begin sanitization + info = escape(info, options, escapeOptions); + + return info +} + +/** + * Updates the document's html tag attributes + * + * @param {Object} attrs - the new document html attributes + * @param {HTMLElement} tag - the HTMLElement tag to update with new attrs + */ +function updateAttribute(ref, attrs, tag) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + var vueMetaAttrString = tag.getAttribute(attribute); + var vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + var toRemove = toArray(vueMetaAttrs); + + var keepIndexes = []; + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + var value = includes(booleanHtmlAttributes, attr) + ? '' + : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]; + + tag.setAttribute(attr, value || ''); + + if (!includes(vueMetaAttrs, attr)) { + vueMetaAttrs.push(attr); + } + + // filter below wont ever check -1 + keepIndexes.push(toRemove.indexOf(attr)); + } + } + + var removedAttributesCount = toRemove + .filter(function (el, index) { return !includes(keepIndexes, index); }) + .reduce(function (acc, attr) { + tag.removeAttribute(attr); + return acc + 1 + }, 0); + + if (vueMetaAttrs.length === removedAttributesCount) { + tag.removeAttribute(attribute); + } else { + tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(',')); + } +} + +/** + * Updates the document title + * + * @param {String} title - the new title of the document + */ +function updateTitle(title) { + if ( title === void 0 ) title = document.title; + + document.title = title; +} + +/** + * Updates meta tags inside and on the client. Borrowed from `react-helmet`: + * https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245 + * + * @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag + * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base + * @return {Object} - a representation of what tags changed + */ +function updateTag(appId, ref, type, tags, headTag, bodyTag) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + var tagIDKeyName = ref.tagIDKeyName; + + var oldHeadTags = toArray(headTag.querySelectorAll((type + "[" + attribute + "=\"" + appId + "\"], " + type + "[data-" + tagIDKeyName + "]"))); + var oldBodyTags = toArray(bodyTag.querySelectorAll((type + "[" + attribute + "=\"" + appId + "\"][data-body=\"true\"], " + type + "[data-" + tagIDKeyName + "][data-body=\"true\"]"))); + var dataAttributes = [tagIDKeyName, 'body']; + var newTags = []; + + if (tags.length > 1) { + // remove duplicates that could have been found by merging tags + // which include a mixin with metaInfo and that mixin is used + // by multiple components on the same page + var found = []; + tags = tags.filter(function (x) { + var k = JSON.stringify(x); + var res = !includes(found, k); + found.push(k); + return res + }); + } + + if (tags.length) { + tags.forEach(function (tag) { + var newElement = document.createElement(type); + + newElement.setAttribute(attribute, appId); + + var oldTags = tag.body !== true ? oldHeadTags : oldBodyTags; + + for (var attr in tag) { + if (tag.hasOwnProperty(attr)) { + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + } else if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + } else { + var _attr = includes(dataAttributes, attr) + ? ("data-" + attr) + : attr; + var value = isUndefined(tag[attr]) || includes(booleanHtmlAttributes, attr) ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + } + } + + // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + var indexToDelete; + var hasEqualElement = oldTags.some(function (existingTag, index) { + indexToDelete = index; + return newElement.isEqualNode(existingTag) + }); + + if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { + oldTags.splice(indexToDelete, 1); + } else { + newTags.push(newElement); + } + }); + } + + var oldTags = oldHeadTags.concat(oldBodyTags); + oldTags.forEach(function (tag) { return tag.parentNode.removeChild(tag); }); + newTags.forEach(function (tag) { + if (tag.getAttribute('data-body') === 'true') { + bodyTag.appendChild(tag); + } else { + headTag.appendChild(tag); + } + }); + + return { oldTags: oldTags, newTags: newTags } +} + +function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag] +} + +/** + * Performs client-side updates when new meta info is received + * + * @param {Object} newInfo - the meta info to update to + */ +function updateClientMetaInfo(appId, options, newInfo) { + if ( options === void 0 ) options = {}; + + var ssrAttribute = options.ssrAttribute; + + // only cache tags for current update + var tags = {}; + + var htmlTag = getTag(tags, 'html'); + + // if this is a server render, then dont update + if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + // remove the server render attribute so we can update on (next) changes + htmlTag.removeAttribute(ssrAttribute); + return false + } + + // initialize tracked changes + var addedTags = {}; + var removedTags = {}; + + for (var type in newInfo) { + // ignore these + if (includes(metaInfoOptionKeys, type)) { + continue + } + + if (type === 'title') { + // update the title + updateTitle(newInfo.title); + continue + } + + if (includes(metaInfoAttributeKeys, type)) { + var tagName = type.substr(0, 4); + updateAttribute(options, newInfo[type], getTag(tags, tagName)); + continue + } + + // tags should always be an array, ignore if it isnt + if (!isArray(newInfo[type])) { + continue + } + + var ref = updateTag( + appId, + options, + type, + newInfo[type], + getTag(tags, 'head'), + getTag(tags, 'body') + ); + var oldTags = ref.oldTags; + var newTags = ref.newTags; + + if (newTags.length) { + addedTags[type] = newTags; + removedTags[type] = oldTags; + } + } + + return { addedTags: addedTags, removedTags: removedTags } +} + +function _refresh(options) { + if ( options === void 0 ) options = {}; + + /** + * When called, will update the current meta info with new meta info. + * Useful when updating meta info as the result of an asynchronous + * action that resolves after the initial render takes place. + * + * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion + * to implement this method. + * + * @return {Object} - new meta info + */ + return function refresh() { + var metaInfo = getMetaInfo(options, this.$root, clientSequences); + + var appId = this.$root._vueMeta.appId; + var tags = updateClientMetaInfo(appId, options, metaInfo); + // emit "event" with new info + if (tags && isFunction(metaInfo.changed)) { + metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags); + } + + return { vm: this, metaInfo: metaInfo, tags: tags } + } +} + +/** + * Generates tag attributes for use on the server. + * + * @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate + * @param {Object} data - the attributes to generate + * @return {Object} - the attribute generator + */ +function attributeGenerator(ref, type, data) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + return { + text: function text() { + var attributeStr = ''; + var watchedAttrs = []; + + for (var attr in data) { + if (data.hasOwnProperty(attr)) { + watchedAttrs.push(attr); + + attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) + ? attr + : (attr + "=\"" + (isArray(data[attr]) ? data[attr].join(' ') : data[attr]) + "\""); + + attributeStr += ' '; + } + } + + attributeStr += attribute + "=\"" + ((watchedAttrs.sort()).join(',')) + "\""; + return attributeStr + } + } +} + +/** + * Generates title output for the server + * + * @param {'title'} type - the string "title" + * @param {String} data - the title text + * @return {Object} - the title generator + */ +function titleGenerator(appId, ref, type, data) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + return { + text: function text() { + return ("<" + type + ">" + data + "") + } + } +} + +/** + * Generates meta, base, link, style, script, noscript tags for use on the server + * + * @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag + * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base + * @return {Object} - the tag generator + */ +function tagGenerator(appId, ref, type, tags) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + var tagIDKeyName = ref.tagIDKeyName; + + return { + text: function text(ref) { + if ( ref === void 0 ) ref = {}; + var body = ref.body; if ( body === void 0 ) body = false; + + // build a string containing all tags of this type + return tags.reduce(function (tagsStr, tag) { + var tagKeys = Object.keys(tag); + + if (tagKeys.length === 0) { + return tagsStr // Bail on empty tag object + } + + if (Boolean(tag.body) !== body) { + return tagsStr + } + + // build a string containing all attributes of this tag + var attrs = tagKeys.reduce(function (attrsStr, attr) { + // these attributes are treated as children on the tag + if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') { + return attrsStr + } + + // these form the attribute list for this tag + var prefix = ''; + if ([tagIDKeyName, 'body'].includes(attr)) { + prefix = 'data-'; + } + + return isUndefined(tag[attr]) || booleanHtmlAttributes.includes(attr) + ? (attrsStr + " " + prefix + attr) + : (attrsStr + " " + prefix + attr + "=\"" + (tag[attr]) + "\"") + }, ''); + + // grab child content from one of these attributes, if possible + var content = tag.innerHTML || tag.cssText || ''; + + // generate tag exactly without any other redundant attribute + var observeTag = tag.once + ? '' + : (attribute + "=\"" + appId + "\""); + + // these tags have no end tag + var hasEndTag = !tagsWithoutEndTag.includes(type); + + // these tag types will have content inserted + var hasContent = hasEndTag && tagsWithInnerContent.includes(type); + + // the final string for this specific tag + return !hasContent + ? (tagsStr + "<" + type + " " + observeTag + attrs + (hasEndTag ? '/' : '') + ">") + : (tagsStr + "<" + type + " " + observeTag + attrs + ">" + content + "") + }, '') + } + } +} + +/** + * Converts a meta info property to one that can be stringified on the server + * + * @param {String} type - the type of data to convert + * @param {(String|Object|Array)} data - the data value + * @return {Object} - the new injector + */ + +function generateServerInjector(appId, options, type, data) { + if (type === 'title') { + return titleGenerator(appId, options, type, data) + } + + if (metaInfoAttributeKeys.includes(type)) { + return attributeGenerator(options, type, data) + } + + return tagGenerator(appId, options, type, data) +} + +function _inject(options) { + if ( options === void 0 ) options = {}; + + /** + * Converts the state of the meta info object such that each item + * can be compiled to a tag string on the server + * + * @this {Object} - Vue instance - ideally the root component + * @return {Object} - server meta info with `toString` methods + */ + return function inject() { + // get meta info with sensible defaults + var metaInfo = getMetaInfo(options, this.$root, serverSequences); + + // generate server injectors + for (var key in metaInfo) { + if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) { + metaInfo[key] = generateServerInjector('ssr', options, key, metaInfo[key]); + } + } + + return metaInfo + } +} + +function _$meta(options) { + if ( options === void 0 ) options = {}; + + var _refresh$1 = _refresh(options); + var _inject$1 = _inject(options); + + /** + * Returns an injector for server-side rendering. + * @this {Object} - the Vue instance (a root component) + * @return {Object} - injector + */ + return function $meta() { + return { + getOptions: function () { return getOptions(options); }, + refresh: _refresh$1.bind(this), + inject: _inject$1.bind(this), + pause: pause.bind(this), + resume: resume.bind(this) + } + } +} + +/** + * Plugin install function. + * @param {Function} Vue - the Vue constructor. + */ +function install(Vue, options) { + if ( options === void 0 ) options = {}; + + if (Vue.__vuemeta_installed) { + return + } + Vue.__vuemeta_installed = true; + + options = setOptions(options); + + Vue.prototype.$meta = _$meta(options); + + Vue.mixin(createMixin(Vue, options)); +} + +var index = { + version: version, + install: install, + hasMetaInfo: hasMetaInfo +}; + +module.exports = index; diff --git a/dist/vue-meta.esm.browser.js b/dist/vue-meta.esm.browser.js new file mode 100644 index 0000000..93adf7b --- /dev/null +++ b/dist/vue-meta.esm.browser.js @@ -0,0 +1,1042 @@ +/** + * vue-meta v2.0.3 + * (c) 2019 + * - Declan de Wet + * - Sébastien Chopin (@Atinux) + * - All the amazing contributors + * @license MIT + */ + +import deepmerge from 'deepmerge'; + +var version = "2.0.3"; + +// store an id to keep track of DOM updates +let batchId = null; + +function triggerUpdate(vm, hookName) { + // if an update was triggered during initialization or when an update was triggered by the + // metaInfo watcher, set initialized to null + // then we keep falsy value but know we need to run a triggerUpdate after initialization + if (!vm.$root._vueMeta.initialized && (vm.$root._vueMeta.initializing || hookName === 'watcher')) { + vm.$root._vueMeta.initialized = null; + } + + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { + // batch potential DOM updates to prevent extraneous re-rendering + batchUpdate(() => vm.$meta().refresh()); + } +} + +/** + * Performs a batched update. + * + * @param {(null|Number)} id - the ID of this update + * @param {Function} callback - the update to perform + * @return {Number} id - a new ID + */ +function batchUpdate(callback, timeout = 10) { + clearTimeout(batchId); + + batchId = setTimeout(() => { + callback(); + }, timeout); + + return batchId +} + +/** + * checks if passed argument is an array + * @param {any} arg - the object to check + * @return {Boolean} - true if `arg` is an array + */ +function isArray(arg) { + return Array.isArray(arg) +} + +function isUndefined(arg) { + return typeof arg === 'undefined' +} + +function isObject(arg) { + return typeof arg === 'object' +} + +function isFunction(arg) { + return typeof arg === 'function' +} + +function isString(arg) { + return typeof arg === 'string' +} + +function ensureIsArray(arg, key) { + if (!key || !isObject(arg)) { + return isArray(arg) ? arg : [] + } + + if (!isArray(arg[key])) { + arg[key] = []; + } + return arg +} + +function ensuredPush(object, key, el) { + ensureIsArray(object, key); + + object[key].push(el); +} + +// Vue $root instance has a _vueMeta object property, otherwise its a boolean true +function hasMetaInfo(vm = this) { + return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) +} + +// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has +function inMetaInfoBranch(vm = this) { + return vm && !isUndefined(vm._vueMeta) +} + +function addNavGuards(vm) { + // return when nav guards already added or no router exists + if (vm.$root._vueMeta.navGuards || !vm.$root.$router) { + /* istanbul ignore next */ + return + } + + vm.$root._vueMeta.navGuards = true; + + const $router = vm.$root.$router; + const $meta = vm.$root.$meta(); + + $router.beforeEach((to, from, next) => { + $meta.pause(); + next(); + }); + + $router.afterEach(() => { + const { metaInfo } = $meta.resume(); + if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) { + metaInfo.afterNavigation(metaInfo); + } + }); +} + +let appId = 1; + +function createMixin(Vue, options) { + // for which Vue lifecycle hooks should the metaInfo be refreshed + const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + + // watch for client side component updates + return { + beforeCreate() { + Object.defineProperty(this, '_hasMetaInfo', { + configurable: true, + get() { + // Show deprecation warning once when devtools enabled + if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) { + console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); // eslint-disable-line no-console + this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; + } + return hasMetaInfo(this) + } + }); + + // Add a marker to know if it uses metaInfo + // _vnode is used to know that it's attached to a real component + // useful if we use some mixin to add some meta tags (like nuxt-i18n) + if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) { + if (!this.$root._vueMeta) { + this.$root._vueMeta = { appId }; + appId++; + } + + // to speed up updates we keep track of branches which have a component with vue-meta info defined + // if _vueMeta = true it has info, if _vueMeta = false a child has info + if (!this._vueMeta) { + this._vueMeta = true; + + let p = this.$parent; + while (p && p !== this.$root) { + if (isUndefined(p._vueMeta)) { + p._vueMeta = false; + } + p = p.$parent; + } + } + + // coerce function-style metaInfo to a computed prop so we can observe + // it on creation + if (isFunction(this.$options[options.keyName])) { + if (!this.$options.computed) { + this.$options.computed = {}; + } + this.$options.computed.$metaInfo = this.$options[options.keyName]; + + if (!this.$isServer) { + // if computed $metaInfo exists, watch it for updates & trigger a refresh + // when it changes (i.e. automatically handle async actions that affect metaInfo) + // credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux) + ensuredPush(this.$options, 'created', () => { + this.$watch('$metaInfo', function () { + triggerUpdate(this, 'watcher'); + }); + }); + } + } + + // force an initial refresh on page load and prevent other lifecycleHooks + // to triggerUpdate until this initial refresh is finished + // this is to make sure that when a page is opened in an inactive tab which + // has throttled rAF/timers we still immediately set the page title + if (isUndefined(this.$root._vueMeta.initialized)) { + this.$root._vueMeta.initialized = this.$isServer; + + if (!this.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'beforeMount', () => { + // if this Vue-app was server rendered, set the appId to 'ssr' + // only one SSR app per page is supported + if (this.$root.$el && this.$root.$el.hasAttribute('data-server-rendered')) { + this.$root._vueMeta.appId = 'ssr'; + } + }); + + // we use the mounted hook here as on page load + ensuredPush(this.$options, 'mounted', () => { + if (!this.$root._vueMeta.initialized) { + // used in triggerUpdate to check if a change was triggered + // during initialization + this.$root._vueMeta.initializing = true; + + // refresh meta in nextTick so all child components have loaded + this.$nextTick(function () { + const { tags, metaInfo } = this.$root.$meta().refresh(); + + // After ssr hydration (identifier by tags === false) check + // if initialized was set to null in triggerUpdate. That'd mean + // that during initilazation changes where triggered which need + // to be applied OR a metaInfo watcher was triggered before the + // current hook was called + // (during initialization all changes are blocked) + if (tags === false && this.$root._vueMeta.initialized === null) { + this.$nextTick(() => triggerUpdate(this, 'initializing')); + } + + this.$root._vueMeta.initialized = true; + delete this.$root._vueMeta.initializing; + + // add the navigation guards if they havent been added yet + // they are needed for the afterNavigation callback + if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) { + addNavGuards(this); + } + }); + } + }); + + // add the navigation guards if requested + if (options.refreshOnceOnNavigation) { + addNavGuards(this); + } + } + } + + // do not trigger refresh on the server side + if (!this.$isServer) { + // no need to add this hooks on server side + updateOnLifecycleHook.forEach((lifecycleHook) => { + ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook)); + }); + + // re-render meta data when returning from a child component to parent + ensuredPush(this.$options, 'destroyed', () => { + // Wait that element is hidden before refreshing meta tags (to support animations) + const interval = setInterval(() => { + if (this.$el && this.$el.offsetParent !== null) { + /* istanbul ignore next line */ + return + } + + clearInterval(interval); + + if (!this.$parent) { + /* istanbul ignore next line */ + return + } + + triggerUpdate(this, 'destroyed'); + }, 50); + }); + } + } + } + } +} + +/** + * These are constant variables used throughout the application. + */ + +// set some sane defaults +const defaultInfo = { + title: '', + titleChunk: '', + titleTemplate: '%s', + htmlAttrs: {}, + bodyAttrs: {}, + headAttrs: {}, + base: [], + link: [], + meta: [], + style: [], + script: [], + noscript: [], + __dangerouslyDisableSanitizers: [], + __dangerouslyDisableSanitizersByTagID: {} +}; + +// This is the name of the component option that contains all the information that +// gets converted to the various meta tags & attributes for the page. +const keyName = 'metaInfo'; + +// This is the attribute vue-meta arguments on elements to know which it should +// manage and which it should ignore. +const attribute = 'data-vue-meta'; + +// This is the attribute that goes on the `html` tag to inform `vue-meta` +// that the server has already generated the meta tags for the initial render. +const ssrAttribute = 'data-vue-meta-server-rendered'; + +// This is the property that tells vue-meta to overwrite (instead of append) +// an item in a tag list. For example, if you have two `meta` tag list items +// that both have `vmid` of "description", then vue-meta will overwrite the +// shallowest one with the deepest one. +const tagIDKeyName = 'vmid'; + +// This is the key name for possible meta templates +const metaTemplateKeyName = 'template'; + +// This is the key name for the content-holding property +const contentKeyName = 'content'; + +const defaultOptions = { + keyName, + attribute, + ssrAttribute, + tagIDKeyName, + contentKeyName, + metaTemplateKeyName +}; + +// List of metaInfo property keys which are configuration options (and dont generate html) +const metaInfoOptionKeys = [ + 'titleChunk', + 'titleTemplate', + 'changed', + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' +]; + +// The metaInfo property keys which are used to disable escaping +const disableOptionKeys = [ + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' +]; + +// List of metaInfo property keys which only generates attributes and no tags +const metaInfoAttributeKeys = [ + 'htmlAttrs', + 'headAttrs', + 'bodyAttrs' +]; + +// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202 +const booleanHtmlAttributes = [ + 'allowfullscreen', + 'amp', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'compact', + 'controls', + 'declare', + 'default', + 'defaultchecked', + 'defaultmuted', + 'defaultselected', + 'defer', + 'disabled', + 'enabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'inert', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nohref', + 'noresize', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'pauseonexit', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'sortable', + 'truespeed', + 'typemustmatch', + 'visible' +]; + +// eslint-disable-next-line no-console +const showWarningNotSupported = () => console.warn('This vue app/component has no vue-meta configuration'); + +function setOptions(options) { + // combine options + options = isObject(options) ? options : {}; + + for (const key in defaultOptions) { + if (!options[key]) { + options[key] = defaultOptions[key]; + } + } + + return options +} + +function getOptions(options) { + const optionsCopy = {}; + for (const key in options) { + optionsCopy[key] = options[key]; + } + return optionsCopy +} + +function pause(refresh = true) { + this.$root._vueMeta.paused = true; + + return () => resume(refresh) +} + +function resume(refresh = true) { + this.$root._vueMeta.paused = false; + + if (refresh) { + return this.$root.$meta().refresh() + } +} + +function applyTemplate({ component, metaTemplateKeyName, contentKeyName }, headObject, template, chunk) { + if (isUndefined(template)) { + template = headObject[metaTemplateKeyName]; + delete headObject[metaTemplateKeyName]; + } + + // return early if no template defined + if (!template) { + return false + } + + if (isUndefined(chunk)) { + chunk = headObject[contentKeyName]; + } + + headObject[contentKeyName] = isFunction(template) + ? template.call(component, chunk) + : template.replace(/%s/g, chunk); + + return true +} + +/* + * To reduce build size, this file provides simple polyfills without + * overly excessive type checking and without modifying + * the global Array.prototype + * The polyfills are automatically removed in the commonjs build + * Also, only files in client/ & shared/ should use these functions + * files in server/ still use normal js function + */ + +function findIndex(array, predicate) { + return array.findIndex(predicate, arguments[2]) +} + +function toArray(arg) { + return Array.from(arg) +} + +function includes(array, value) { + return array.includes(value) +} + +const clientSequences = [ + [/&/g, '\u0026'], + [//g, '\u003e'], + [/"/g, '\u0022'], + [/'/g, '\u0027'] +]; + +// sanitizes potentially dangerous characters +function escape(info, options, escapeOptions) { + const { tagIDKeyName } = options; + const { doEscape = v => v } = escapeOptions; + const escaped = {}; + + for (const key in info) { + const value = info[key]; + + // no need to escape configuration options + if (includes(metaInfoOptionKeys, key)) { + escaped[key] = value; + continue + } + + let [ disableKey ] = disableOptionKeys; + if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) { + // this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers + escaped[key] = value; + continue + } + + const tagId = info[tagIDKeyName]; + if (tagId) { + disableKey = disableOptionKeys[1]; + + // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped + if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) { + escaped[key] = value; + continue + } + } + + if (isString(value)) { + escaped[key] = doEscape(value); + } else if (isArray(value)) { + escaped[key] = value.map((v) => { + return isObject(v) + ? escape(v, options, escapeOptions) + : doEscape(v) + }); + } else if (isObject(value)) { + escaped[key] = escape(value, options, escapeOptions); + } else { + escaped[key] = value; + } + } + + return escaped +} + +function arrayMerge({ component, tagIDKeyName, metaTemplateKeyName, contentKeyName }, target, source) { + // we concat the arrays without merging objects contained in, + // but we check for a `vmid` property on each object in the array + // using an O(1) lookup associative array exploit + const destination = []; + + target.forEach((targetItem, targetIndex) => { + // no tagID so no need to check for duplicity + if (!targetItem[tagIDKeyName]) { + destination.push(targetItem); + return + } + + const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName]); + const sourceItem = source[sourceIndex]; + + // source doesnt contain any duplicate vmid's, we can keep targetItem + if (sourceIndex === -1) { + destination.push(targetItem); + return + } + + // when sourceItem explictly defines contentKeyName or innerHTML as undefined, its + // an indication that we need to skip the default behaviour or child has preference over parent + // which means we keep the targetItem and ignore/remove the sourceItem + if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) || + (sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) { + destination.push(targetItem); + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem + + // if source specifies null as content then ignore both the target as the source + if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) { + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // now we only need to check if the target has a template to combine it with the source + const targetTemplate = targetItem[metaTemplateKeyName]; + if (!targetTemplate) { + return + } + + const sourceTemplate = sourceItem[metaTemplateKeyName]; + + if (!sourceTemplate) { + // use parent template and child content + applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, targetTemplate); + } else if (!sourceItem[contentKeyName]) { + // use child template and parent content + applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + } + }); + + return destination.concat(source) +} + +function merge(target, source, options = {}) { + // remove properties explicitly set to false so child components can + // optionally _not_ overwrite the parents content + // (for array properties this is checked in arrayMerge) + if (source.hasOwnProperty('title') && source.title === undefined) { + delete source.title; + } + + metaInfoAttributeKeys.forEach((attrKey) => { + if (!source[attrKey]) { + return + } + + for (const key in source[attrKey]) { + if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { + delete source[attrKey][key]; + } + } + }); + + return deepmerge(target, source, { + arrayMerge: (t, s) => arrayMerge(options, t, s) + }) +} + +/** + * Returns the `opts.option` $option value of the given `opts.component`. + * If methods are encountered, they will be bound to the component context. + * If `opts.deep` is true, will recursively merge all child component + * `opts.option` $option values into the returned result. + * + * @param {Object} opts - options + * @param {Object} opts.component - Vue component to fetch option data from + * @param {Boolean} opts.deep - look for data in child components as well? + * @param {Function} opts.arrayMerge - how should arrays be merged? + * @param {String} opts.keyName - the name of the option to look for + * @param {Object} [result={}] - result so far + * @return {Object} result - final aggregated result + */ +function getComponentOption(options = {}, component, result = {}) { + const { keyName, metaTemplateKeyName, tagIDKeyName } = options; + const { $options, $children } = component; + + if (component._inactive) { + return result + } + + // only collect option data if it exists + if ($options[keyName]) { + let data = $options[keyName]; + + // if option is a function, replace it with it's result + if (isFunction(data)) { + data = data.call(component); + } + + // ignore data if its not an object, then we keep our previous result + if (!isObject(data)) { + return result + } + + // merge with existing options + result = merge(result, data, options); + } + + // collect & aggregate child options if deep = true + if ($children.length) { + $children.forEach((childComponent) => { + // check if the childComponent is in a branch + // return otherwise so we dont walk all component branches unnecessarily + if (!inMetaInfoBranch(childComponent)) { + return + } + + result = getComponentOption(options, childComponent, result); + }); + } + + if (metaTemplateKeyName && result.meta) { + // apply templates if needed + result.meta.forEach(metaObject => applyTemplate(options, metaObject)); + + // remove meta items with duplicate vmid's + result.meta = result.meta.filter((metaItem, index, arr) => { + return ( + // keep meta item if it doesnt has a vmid + !metaItem.hasOwnProperty(tagIDKeyName) || + // or if it's the first item in the array with this vmid + index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName]) + ) + }); + } + + return result +} + +/** + * Returns the correct meta info for the given component + * (child components will overwrite parent meta info) + * + * @param {Object} component - the Vue instance to get meta info from + * @return {Object} - returned meta info + */ +function getMetaInfo(options = {}, component, escapeSequences = []) { + // collect & aggregate all metaInfo $options + let info = getComponentOption(options, component, defaultInfo); + + // Remove all "template" tags from meta + + // backup the title chunk in case user wants access to it + if (info.title) { + info.titleChunk = info.title; + } + + // replace title with populated template + if (info.titleTemplate && info.titleTemplate !== '%s') { + applyTemplate({ component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || ''); + } + + // convert base tag to an array so it can be handled the same way + // as the other tags + if (info.base) { + info.base = Object.keys(info.base).length ? [info.base] : []; + } + + const escapeOptions = { + doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value) + }; + + disableOptionKeys.forEach((disableKey, index) => { + if (index === 0) { + ensureIsArray(info, disableKey); + } else if (index === 1) { + for (const key in info[disableKey]) { + ensureIsArray(info[disableKey], key); + } + } + + escapeOptions[disableKey] = info[disableKey]; + }); + + // begin sanitization + info = escape(info, options, escapeOptions); + + return info +} + +/** + * Updates the document's html tag attributes + * + * @param {Object} attrs - the new document html attributes + * @param {HTMLElement} tag - the HTMLElement tag to update with new attrs + */ +function updateAttribute({ attribute } = {}, attrs, tag) { + const vueMetaAttrString = tag.getAttribute(attribute); + const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + const toRemove = toArray(vueMetaAttrs); + + const keepIndexes = []; + for (const attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + const value = includes(booleanHtmlAttributes, attr) + ? '' + : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]; + + tag.setAttribute(attr, value || ''); + + if (!includes(vueMetaAttrs, attr)) { + vueMetaAttrs.push(attr); + } + + // filter below wont ever check -1 + keepIndexes.push(toRemove.indexOf(attr)); + } + } + + const removedAttributesCount = toRemove + .filter((el, index) => !includes(keepIndexes, index)) + .reduce((acc, attr) => { + tag.removeAttribute(attr); + return acc + 1 + }, 0); + + if (vueMetaAttrs.length === removedAttributesCount) { + tag.removeAttribute(attribute); + } else { + tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(',')); + } +} + +/** + * Updates the document title + * + * @param {String} title - the new title of the document + */ +function updateTitle(title = document.title) { + document.title = title; +} + +/** + * Updates meta tags inside and on the client. Borrowed from `react-helmet`: + * https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245 + * + * @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag + * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base + * @return {Object} - a representation of what tags changed + */ +function updateTag(appId, { attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) { + const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}="${appId}"], ${type}[data-${tagIDKeyName}]`)); + const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}="${appId}"][data-body="true"], ${type}[data-${tagIDKeyName}][data-body="true"]`)); + const dataAttributes = [tagIDKeyName, 'body']; + const newTags = []; + + if (tags.length > 1) { + // remove duplicates that could have been found by merging tags + // which include a mixin with metaInfo and that mixin is used + // by multiple components on the same page + const found = []; + tags = tags.filter((x) => { + const k = JSON.stringify(x); + const res = !includes(found, k); + found.push(k); + return res + }); + } + + if (tags.length) { + tags.forEach((tag) => { + const newElement = document.createElement(type); + + newElement.setAttribute(attribute, appId); + + const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags; + + for (const attr in tag) { + if (tag.hasOwnProperty(attr)) { + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + } else if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + } else { + const _attr = includes(dataAttributes, attr) + ? `data-${attr}` + : attr; + const value = isUndefined(tag[attr]) || includes(booleanHtmlAttributes, attr) ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + } + } + + // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + let indexToDelete; + const hasEqualElement = oldTags.some((existingTag, index) => { + indexToDelete = index; + return newElement.isEqualNode(existingTag) + }); + + if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { + oldTags.splice(indexToDelete, 1); + } else { + newTags.push(newElement); + } + }); + } + + const oldTags = oldHeadTags.concat(oldBodyTags); + oldTags.forEach(tag => tag.parentNode.removeChild(tag)); + newTags.forEach((tag) => { + if (tag.getAttribute('data-body') === 'true') { + bodyTag.appendChild(tag); + } else { + headTag.appendChild(tag); + } + }); + + return { oldTags, newTags } +} + +function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag] +} + +/** + * Performs client-side updates when new meta info is received + * + * @param {Object} newInfo - the meta info to update to + */ +function updateClientMetaInfo(appId, options = {}, newInfo) { + const { ssrAttribute } = options; + + // only cache tags for current update + const tags = {}; + + const htmlTag = getTag(tags, 'html'); + + // if this is a server render, then dont update + if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + // remove the server render attribute so we can update on (next) changes + htmlTag.removeAttribute(ssrAttribute); + return false + } + + // initialize tracked changes + const addedTags = {}; + const removedTags = {}; + + for (const type in newInfo) { + // ignore these + if (includes(metaInfoOptionKeys, type)) { + continue + } + + if (type === 'title') { + // update the title + updateTitle(newInfo.title); + continue + } + + if (includes(metaInfoAttributeKeys, type)) { + const tagName = type.substr(0, 4); + updateAttribute(options, newInfo[type], getTag(tags, tagName)); + continue + } + + // tags should always be an array, ignore if it isnt + if (!isArray(newInfo[type])) { + continue + } + + const { oldTags, newTags } = updateTag( + appId, + options, + type, + newInfo[type], + getTag(tags, 'head'), + getTag(tags, 'body') + ); + + if (newTags.length) { + addedTags[type] = newTags; + removedTags[type] = oldTags; + } + } + + return { addedTags, removedTags } +} + +function _refresh(options = {}) { + /** + * When called, will update the current meta info with new meta info. + * Useful when updating meta info as the result of an asynchronous + * action that resolves after the initial render takes place. + * + * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion + * to implement this method. + * + * @return {Object} - new meta info + */ + return function refresh() { + const metaInfo = getMetaInfo(options, this.$root, clientSequences); + + const appId = this.$root._vueMeta.appId; + const tags = updateClientMetaInfo(appId, options, metaInfo); + // emit "event" with new info + if (tags && isFunction(metaInfo.changed)) { + metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags); + } + + return { vm: this, metaInfo, tags } + } +} + +function _$meta(options = {}) { + const _refresh$1 = _refresh(options); + const inject = () => {}; + + /** + * Returns an injector for server-side rendering. + * @this {Object} - the Vue instance (a root component) + * @return {Object} - injector + */ + return function $meta() { + if (!this.$root._vueMeta) { + return { + getOptions: showWarningNotSupported, + refresh: showWarningNotSupported, + inject: showWarningNotSupported, + pause: showWarningNotSupported, + resume: showWarningNotSupported + } + } + + return { + getOptions: () => getOptions(options), + refresh: _refresh$1.bind(this), + inject, + pause: pause.bind(this), + resume: resume.bind(this) + } + } +} + +/** + * Plugin install function. + * @param {Function} Vue - the Vue constructor. + */ +function install(Vue, options = {}) { + if (Vue.__vuemeta_installed) { + return + } + Vue.__vuemeta_installed = true; + + options = setOptions(options); + + Vue.prototype.$meta = _$meta(options); + + Vue.mixin(createMixin(Vue, options)); +} + +// automatic install +if (!isUndefined(window) && !isUndefined(window.Vue)) { + /* istanbul ignore next */ + install(window.Vue); +} + +var browser = { + version, + install, + hasMetaInfo +}; + +export default browser; diff --git a/dist/vue-meta.esm.browser.min.js b/dist/vue-meta.esm.browser.min.js new file mode 100644 index 0000000..2bc3c02 --- /dev/null +++ b/dist/vue-meta.esm.browser.min.js @@ -0,0 +1 @@ +import e from"deepmerge";let t=null;function n(e,n){e.$root._vueMeta.initialized||!e.$root._vueMeta.initializing&&"watcher"!==n||(e.$root._vueMeta.initialized=null),e.$root._vueMeta.initialized&&!e.$root._vueMeta.paused&&function(e,n=10){clearTimeout(t),t=setTimeout(()=>{e()},n)}(()=>e.$meta().refresh())}function i(e){return Array.isArray(e)}function o(e){return void 0===e}function a(e){return"object"==typeof e}function r(e){return"function"==typeof e}function s(e,t){return t&&a(e)?(i(e[t])||(e[t]=[]),e):i(e)?e:[]}function u(e,t,n){s(e,t),e[t].push(n)}function c(e=this){return e&&(!0===e._vueMeta||a(e._vueMeta))}function l(e){if(e.$root._vueMeta.navGuards||!e.$root.$router)return;e.$root._vueMeta.navGuards=!0;const t=e.$root.$router,n=e.$root.$meta();t.beforeEach((e,t,i)=>{n.pause(),i()}),t.afterEach(()=>{const{metaInfo:e}=n.resume();e&&e.afterNavigation&&r(e.afterNavigation)&&e.afterNavigation(e)})}let d=1;const h={title:"",titleChunk:"",titleTemplate:"%s",htmlAttrs:{},bodyAttrs:{},headAttrs:{},base:[],link:[],meta:[],style:[],script:[],noscript:[],__dangerouslyDisableSanitizers:[],__dangerouslyDisableSanitizersByTagID:{}},f={keyName:"metaInfo",attribute:"data-vue-meta",ssrAttribute:"data-vue-meta-server-rendered",tagIDKeyName:"vmid",contentKeyName:"content",metaTemplateKeyName:"template"},m=["titleChunk","titleTemplate","changed","__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],p=["__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],v=["htmlAttrs","headAttrs","bodyAttrs"],$=["allowfullscreen","amp","async","autofocus","autoplay","checked","compact","controls","declare","default","defaultchecked","defaultmuted","defaultselected","defer","disabled","enabled","formnovalidate","hidden","indeterminate","inert","ismap","itemscope","loop","multiple","muted","nohref","noresize","noshade","novalidate","nowrap","open","pauseonexit","readonly","required","reversed","scoped","seamless","selected","sortable","truespeed","typemustmatch","visible"],y=()=>console.warn("This vue app/component has no vue-meta configuration");function g(e=!0){return this.$root._vueMeta.paused=!0,()=>b(e)}function b(e=!0){if(this.$root._vueMeta.paused=!1,e)return this.$root.$meta().refresh()}function _({component:e,metaTemplateKeyName:t,contentKeyName:n},i,a,s){return o(a)&&(a=i[t],delete i[t]),!!a&&(o(s)&&(s=i[n]),i[n]=r(a)?a.call(e,s):a.replace(/%s/g,s),!0)}function M(e,t){return e.findIndex(t,arguments[2])}function T(e){return Array.from(e)}function N(e,t){return e.includes(t)}const I=[[/&/g,"&"],[//g,">"],[/"/g,'"'],[/'/g,"'"]];function w(t,n,i={}){return n.hasOwnProperty("title")&&void 0===n.title&&delete n.title,v.forEach(e=>{if(n[e])for(const t in n[e])n[e].hasOwnProperty(t)&&void 0===n[e][t]&&delete n[e][t]}),e(t,n,{arrayMerge:(e,t)=>(function({component:e,tagIDKeyName:t,metaTemplateKeyName:n,contentKeyName:i},o,a){const r=[];return o.forEach((o,s)=>{if(!o[t])return void r.push(o);const u=M(a,e=>e[t]===o[t]),c=a[u];if(-1===u)return void r.push(o);if(c.hasOwnProperty(i)&&void 0===c[i]||c.hasOwnProperty("innerHTML")&&void 0===c.innerHTML)return r.push(o),void a.splice(u,1);if(null===c[i]||null===c.innerHTML)return void a.splice(u,1);const l=o[n];l&&(c[n]?c[i]||_({component:e,metaTemplateKeyName:n,contentKeyName:i},c,void 0,o[i]):_({component:e,metaTemplateKeyName:n,contentKeyName:i},c,l))}),r.concat(a)})(i,e,t)})}function A(e={},t,n={}){const{keyName:i,metaTemplateKeyName:s,tagIDKeyName:u}=e,{$options:c,$children:l}=t;if(t._inactive)return n;if(c[i]){let o=c[i];if(r(o)&&(o=o.call(t)),!a(o))return n;n=w(n,o,e)}return l.length&&l.forEach(t=>{(function(e=this){return e&&!o(e._vueMeta)})(t)&&(n=A(e,t,n))}),s&&n.meta&&(n.meta.forEach(t=>_(e,t)),n.meta=n.meta.filter((e,t,n)=>!e.hasOwnProperty(u)||t===M(n,t=>t[u]===e[u]))),n}function z(e={},t,n=[]){let o=A(e,t,h);o.title&&(o.titleChunk=o.title),o.titleTemplate&&"%s"!==o.titleTemplate&&_({component:t,contentKeyName:"title"},o,o.titleTemplate,o.titleChunk||""),o.base&&(o.base=Object.keys(o.base).length?[o.base]:[]);const r={doEscape:e=>n.reduce((e,[t,n])=>e.replace(t,n),e)};return p.forEach((e,t)=>{if(0===t)s(o,e);else if(1===t)for(const t in o[e])s(o[e],t);r[e]=o[e]}),o=function e(t,n,o){const{tagIDKeyName:r}=n,{doEscape:s=(e=>e)}=o,u={};for(const c in t){const l=t[c];if(N(m,c)){u[c]=l;continue}let[d]=p;if(o[d]&&N(o[d],c)){u[c]=l;continue}const h=t[r];h&&(d=p[1],o[d]&&o[d][h]&&N(o[d][h],c))?u[c]=l:"string"==typeof l?u[c]=s(l):i(l)?u[c]=l.map(t=>a(t)?e(t,n,o):s(t)):a(l)?u[c]=e(l,n,o):u[c]=l}return u}(o,e,r)}function D({attribute:e}={},t,n){const o=n.getAttribute(e),a=o?o.split(","):[],r=T(a),s=[];for(const e in t)if(t.hasOwnProperty(e)){const o=N($,e)?"":i(t[e])?t[e].join(" "):t[e];n.setAttribute(e,o||""),N(a,e)||a.push(e),s.push(r.indexOf(e))}const u=r.filter((e,t)=>!N(s,t)).reduce((e,t)=>(n.removeAttribute(t),e+1),0);a.length===u?n.removeAttribute(e):n.setAttribute(e,a.sort().join(","))}function K(e=document.title){document.title=e}function O(e,{attribute:t,tagIDKeyName:n}={},i,a,r,s){const u=T(r.querySelectorAll(`${i}[${t}="${e}"], ${i}[data-${n}]`)),c=T(s.querySelectorAll(`${i}[${t}="${e}"][data-body="true"], ${i}[data-${n}][data-body="true"]`)),l=[n,"body"],d=[];if(a.length>1){const e=[];a=a.filter(t=>{const n=JSON.stringify(t),i=!N(e,n);return e.push(n),i})}a.length&&a.forEach(n=>{const a=document.createElement(i);a.setAttribute(t,e);const r=!0!==n.body?u:c;for(const e in n)if(n.hasOwnProperty(e))if("innerHTML"===e)a.innerHTML=n.innerHTML;else if("cssText"===e)a.styleSheet?a.styleSheet.cssText=n.cssText:a.appendChild(document.createTextNode(n.cssText));else{const t=N(l,e)?`data-${e}`:e,i=o(n[e])||N($,e)?"":n[e];a.setAttribute(t,i)}let s;r.some((e,t)=>(s=t,a.isEqualNode(e)))&&(s||0===s)?r.splice(s,1):d.push(a)});const h=u.concat(c);return h.forEach(e=>e.parentNode.removeChild(e)),d.forEach(e=>{"true"===e.getAttribute("data-body")?s.appendChild(e):r.appendChild(e)}),{oldTags:h,newTags:d}}function k(e,t){return e[t]||(e[t]=document.getElementsByTagName(t)[0]),e[t]}function E(e={}){return function(){const t=z(e,this.$root,I),n=function(e,t={},n){const{ssrAttribute:o}=t,a={},r=k(a,"html");if("ssr"===e&&r.hasAttribute(o))return r.removeAttribute(o),!1;const s={},u={};for(const o in n){if(N(m,o))continue;if("title"===o){K(n.title);continue}if(N(v,o)){const e=o.substr(0,4);D(t,n[o],k(a,e));continue}if(!i(n[o]))continue;const{oldTags:r,newTags:c}=O(e,t,o,n[o],k(a,"head"),k(a,"body"));c.length&&(s[o]=c,u[o]=r)}return{addedTags:s,removedTags:u}}(this.$root._vueMeta.appId,e,t);return n&&r(t.changed)&&t.changed(t,n.addedTags,n.removedTags),{vm:this,metaInfo:t,tags:n}}}function S(e,t={}){e.__vuemeta_installed||(e.__vuemeta_installed=!0,t=function(e){e=a(e)?e:{};for(const t in f)e[t]||(e[t]=f[t]);return e}(t),e.prototype.$meta=function(e={}){const t=E(e),n=()=>{};return function(){return this.$root._vueMeta?{getOptions:()=>(function(e){const t={};for(const n in e)t[n]=e[n];return t})(e),refresh:t.bind(this),inject:n,pause:g.bind(this),resume:b.bind(this)}:{getOptions:y,refresh:y,inject:y,pause:y,resume:y}}}(t),e.mixin(function(e,t){const i=["activated","deactivated","beforeMount"];return{beforeCreate(){if(Object.defineProperty(this,"_hasMetaInfo",{configurable:!0,get(){return e.config.devtools&&!this.$root._vueMeta.hasMetaInfoDeprecationWarningShown&&(console.warn("VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead"),this.$root._vueMeta.hasMetaInfoDeprecationWarningShown=!0),c(this)}}),!o(this.$options[t.keyName])&&null!==this.$options[t.keyName]){if(this.$root._vueMeta||(this.$root._vueMeta={appId:d},d++),!this._vueMeta){this._vueMeta=!0;let e=this.$parent;for(;e&&e!==this.$root;)o(e._vueMeta)&&(e._vueMeta=!1),e=e.$parent}r(this.$options[t.keyName])&&(this.$options.computed||(this.$options.computed={}),this.$options.computed.$metaInfo=this.$options[t.keyName],this.$isServer||u(this.$options,"created",()=>{this.$watch("$metaInfo",function(){n(this,"watcher")})})),o(this.$root._vueMeta.initialized)&&(this.$root._vueMeta.initialized=this.$isServer,this.$root._vueMeta.initialized||(u(this.$options,"beforeMount",()=>{this.$root.$el&&this.$root.$el.hasAttribute("data-server-rendered")&&(this.$root._vueMeta.appId="ssr")}),u(this.$options,"mounted",()=>{this.$root._vueMeta.initialized||(this.$root._vueMeta.initializing=!0,this.$nextTick(function(){const{tags:e,metaInfo:i}=this.$root.$meta().refresh();!1===e&&null===this.$root._vueMeta.initialized&&this.$nextTick(()=>n(this,"initializing")),this.$root._vueMeta.initialized=!0,delete this.$root._vueMeta.initializing,!t.refreshOnceOnNavigation&&i.afterNavigation&&l(this)}))}),t.refreshOnceOnNavigation&&l(this))),this.$isServer||(i.forEach(e=>{u(this.$options,e,()=>n(this,e))}),u(this.$options,"destroyed",()=>{const e=setInterval(()=>{this.$el&&null!==this.$el.offsetParent||(clearInterval(e),this.$parent&&n(this,"destroyed"))},50)}))}}}}(e,t)))}o(window)||o(window.Vue)||S(window.Vue);export default{version:"2.0.3",install:S,hasMetaInfo:c}; diff --git a/dist/vue-meta.esm.js b/dist/vue-meta.esm.js new file mode 100644 index 0000000..1d46f84 --- /dev/null +++ b/dist/vue-meta.esm.js @@ -0,0 +1,1292 @@ +/** + * vue-meta v2.0.3 + * (c) 2019 + * - Declan de Wet + * - Sébastien Chopin (@Atinux) + * - All the amazing contributors + * @license MIT + */ + +import deepmerge from 'deepmerge'; + +var version = "2.0.3"; + +// store an id to keep track of DOM updates +var batchId = null; + +function triggerUpdate(vm, hookName) { + // if an update was triggered during initialization or when an update was triggered by the + // metaInfo watcher, set initialized to null + // then we keep falsy value but know we need to run a triggerUpdate after initialization + if (!vm.$root._vueMeta.initialized && (vm.$root._vueMeta.initializing || hookName === 'watcher')) { + vm.$root._vueMeta.initialized = null; + } + + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { + // batch potential DOM updates to prevent extraneous re-rendering + batchUpdate(function () { return vm.$meta().refresh(); }); + } +} + +/** + * Performs a batched update. + * + * @param {(null|Number)} id - the ID of this update + * @param {Function} callback - the update to perform + * @return {Number} id - a new ID + */ +function batchUpdate(callback, timeout) { + if ( timeout === void 0 ) timeout = 10; + + clearTimeout(batchId); + + batchId = setTimeout(function () { + callback(); + }, timeout); + + return batchId +} + +/** + * checks if passed argument is an array + * @param {any} arg - the object to check + * @return {Boolean} - true if `arg` is an array + */ +function isArray(arg) { + return Array.isArray(arg) +} + +function isUndefined(arg) { + return typeof arg === 'undefined' +} + +function isObject(arg) { + return typeof arg === 'object' +} + +function isFunction(arg) { + return typeof arg === 'function' +} + +function isString(arg) { + return typeof arg === 'string' +} + +function ensureIsArray(arg, key) { + if (!key || !isObject(arg)) { + return isArray(arg) ? arg : [] + } + + if (!isArray(arg[key])) { + arg[key] = []; + } + return arg +} + +function ensuredPush(object, key, el) { + ensureIsArray(object, key); + + object[key].push(el); +} + +// Vue $root instance has a _vueMeta object property, otherwise its a boolean true +function hasMetaInfo(vm) { + if ( vm === void 0 ) vm = this; + + return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) +} + +// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has +function inMetaInfoBranch(vm) { + if ( vm === void 0 ) vm = this; + + return vm && !isUndefined(vm._vueMeta) +} + +function addNavGuards(vm) { + // return when nav guards already added or no router exists + if (vm.$root._vueMeta.navGuards || !vm.$root.$router) { + /* istanbul ignore next */ + return + } + + vm.$root._vueMeta.navGuards = true; + + var $router = vm.$root.$router; + var $meta = vm.$root.$meta(); + + $router.beforeEach(function (to, from, next) { + $meta.pause(); + next(); + }); + + $router.afterEach(function () { + var ref = $meta.resume(); + var metaInfo = ref.metaInfo; + if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) { + metaInfo.afterNavigation(metaInfo); + } + }); +} + +var appId = 1; + +function createMixin(Vue, options) { + // for which Vue lifecycle hooks should the metaInfo be refreshed + var updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + + // watch for client side component updates + return { + beforeCreate: function beforeCreate() { + var this$1 = this; + + Object.defineProperty(this, '_hasMetaInfo', { + configurable: true, + get: function get() { + // Show deprecation warning once when devtools enabled + if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) { + console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); // eslint-disable-line no-console + this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; + } + return hasMetaInfo(this) + } + }); + + // Add a marker to know if it uses metaInfo + // _vnode is used to know that it's attached to a real component + // useful if we use some mixin to add some meta tags (like nuxt-i18n) + if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) { + if (!this.$root._vueMeta) { + this.$root._vueMeta = { appId: appId }; + appId++; + } + + // to speed up updates we keep track of branches which have a component with vue-meta info defined + // if _vueMeta = true it has info, if _vueMeta = false a child has info + if (!this._vueMeta) { + this._vueMeta = true; + + var p = this.$parent; + while (p && p !== this.$root) { + if (isUndefined(p._vueMeta)) { + p._vueMeta = false; + } + p = p.$parent; + } + } + + // coerce function-style metaInfo to a computed prop so we can observe + // it on creation + if (isFunction(this.$options[options.keyName])) { + if (!this.$options.computed) { + this.$options.computed = {}; + } + this.$options.computed.$metaInfo = this.$options[options.keyName]; + + if (!this.$isServer) { + // if computed $metaInfo exists, watch it for updates & trigger a refresh + // when it changes (i.e. automatically handle async actions that affect metaInfo) + // credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux) + ensuredPush(this.$options, 'created', function () { + this$1.$watch('$metaInfo', function () { + triggerUpdate(this, 'watcher'); + }); + }); + } + } + + // force an initial refresh on page load and prevent other lifecycleHooks + // to triggerUpdate until this initial refresh is finished + // this is to make sure that when a page is opened in an inactive tab which + // has throttled rAF/timers we still immediately set the page title + if (isUndefined(this.$root._vueMeta.initialized)) { + this.$root._vueMeta.initialized = this.$isServer; + + if (!this.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'beforeMount', function () { + // if this Vue-app was server rendered, set the appId to 'ssr' + // only one SSR app per page is supported + if (this$1.$root.$el && this$1.$root.$el.hasAttribute('data-server-rendered')) { + this$1.$root._vueMeta.appId = 'ssr'; + } + }); + + // we use the mounted hook here as on page load + ensuredPush(this.$options, 'mounted', function () { + if (!this$1.$root._vueMeta.initialized) { + // used in triggerUpdate to check if a change was triggered + // during initialization + this$1.$root._vueMeta.initializing = true; + + // refresh meta in nextTick so all child components have loaded + this$1.$nextTick(function () { + var this$1 = this; + + var ref = this.$root.$meta().refresh(); + var tags = ref.tags; + var metaInfo = ref.metaInfo; + + // After ssr hydration (identifier by tags === false) check + // if initialized was set to null in triggerUpdate. That'd mean + // that during initilazation changes where triggered which need + // to be applied OR a metaInfo watcher was triggered before the + // current hook was called + // (during initialization all changes are blocked) + if (tags === false && this.$root._vueMeta.initialized === null) { + this.$nextTick(function () { return triggerUpdate(this$1, 'initializing'); }); + } + + this.$root._vueMeta.initialized = true; + delete this.$root._vueMeta.initializing; + + // add the navigation guards if they havent been added yet + // they are needed for the afterNavigation callback + if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) { + addNavGuards(this); + } + }); + } + }); + + // add the navigation guards if requested + if (options.refreshOnceOnNavigation) { + addNavGuards(this); + } + } + } + + // do not trigger refresh on the server side + if (!this.$isServer) { + // no need to add this hooks on server side + updateOnLifecycleHook.forEach(function (lifecycleHook) { + ensuredPush(this$1.$options, lifecycleHook, function () { return triggerUpdate(this$1, lifecycleHook); }); + }); + + // re-render meta data when returning from a child component to parent + ensuredPush(this.$options, 'destroyed', function () { + // Wait that element is hidden before refreshing meta tags (to support animations) + var interval = setInterval(function () { + if (this$1.$el && this$1.$el.offsetParent !== null) { + /* istanbul ignore next line */ + return + } + + clearInterval(interval); + + if (!this$1.$parent) { + /* istanbul ignore next line */ + return + } + + triggerUpdate(this$1, 'destroyed'); + }, 50); + }); + } + } + } + } +} + +/** + * These are constant variables used throughout the application. + */ + +// set some sane defaults +var defaultInfo = { + title: '', + titleChunk: '', + titleTemplate: '%s', + htmlAttrs: {}, + bodyAttrs: {}, + headAttrs: {}, + base: [], + link: [], + meta: [], + style: [], + script: [], + noscript: [], + __dangerouslyDisableSanitizers: [], + __dangerouslyDisableSanitizersByTagID: {} +}; + +// This is the name of the component option that contains all the information that +// gets converted to the various meta tags & attributes for the page. +var keyName = 'metaInfo'; + +// This is the attribute vue-meta arguments on elements to know which it should +// manage and which it should ignore. +var attribute = 'data-vue-meta'; + +// This is the attribute that goes on the `html` tag to inform `vue-meta` +// that the server has already generated the meta tags for the initial render. +var ssrAttribute = 'data-vue-meta-server-rendered'; + +// This is the property that tells vue-meta to overwrite (instead of append) +// an item in a tag list. For example, if you have two `meta` tag list items +// that both have `vmid` of "description", then vue-meta will overwrite the +// shallowest one with the deepest one. +var tagIDKeyName = 'vmid'; + +// This is the key name for possible meta templates +var metaTemplateKeyName = 'template'; + +// This is the key name for the content-holding property +var contentKeyName = 'content'; + +var defaultOptions = { + keyName: keyName, + attribute: attribute, + ssrAttribute: ssrAttribute, + tagIDKeyName: tagIDKeyName, + contentKeyName: contentKeyName, + metaTemplateKeyName: metaTemplateKeyName +}; + +// List of metaInfo property keys which are configuration options (and dont generate html) +var metaInfoOptionKeys = [ + 'titleChunk', + 'titleTemplate', + 'changed', + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' +]; + +// The metaInfo property keys which are used to disable escaping +var disableOptionKeys = [ + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' +]; + +// List of metaInfo property keys which only generates attributes and no tags +var metaInfoAttributeKeys = [ + 'htmlAttrs', + 'headAttrs', + 'bodyAttrs' +]; + +// HTML elements which dont have a head tag (shortened to our needs) +// see: https://www.w3.org/TR/html52/document-metadata.html +var tagsWithoutEndTag = ['base', 'meta', 'link']; + +// HTML elements which can have inner content (shortened to our needs) +var tagsWithInnerContent = ['noscript', 'script', 'style']; + +// Attributes which are inserted as childNodes instead of HTMLAttribute +var tagAttributeAsInnerContent = ['innerHTML', 'cssText']; + +// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202 +var booleanHtmlAttributes = [ + 'allowfullscreen', + 'amp', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'compact', + 'controls', + 'declare', + 'default', + 'defaultchecked', + 'defaultmuted', + 'defaultselected', + 'defer', + 'disabled', + 'enabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'inert', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nohref', + 'noresize', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'pauseonexit', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'sortable', + 'truespeed', + 'typemustmatch', + 'visible' +]; + +function setOptions(options) { + // combine options + options = isObject(options) ? options : {}; + + for (var key in defaultOptions) { + if (!options[key]) { + options[key] = defaultOptions[key]; + } + } + + return options +} + +function getOptions(options) { + var optionsCopy = {}; + for (var key in options) { + optionsCopy[key] = options[key]; + } + return optionsCopy +} + +function pause(refresh) { + if ( refresh === void 0 ) refresh = true; + + this.$root._vueMeta.paused = true; + + return function () { return resume(refresh); } +} + +function resume(refresh) { + if ( refresh === void 0 ) refresh = true; + + this.$root._vueMeta.paused = false; + + if (refresh) { + return this.$root.$meta().refresh() + } +} + +function applyTemplate(ref, headObject, template, chunk) { + var component = ref.component; + var metaTemplateKeyName = ref.metaTemplateKeyName; + var contentKeyName = ref.contentKeyName; + + if (isUndefined(template)) { + template = headObject[metaTemplateKeyName]; + delete headObject[metaTemplateKeyName]; + } + + // return early if no template defined + if (!template) { + return false + } + + if (isUndefined(chunk)) { + chunk = headObject[contentKeyName]; + } + + headObject[contentKeyName] = isFunction(template) + ? template.call(component, chunk) + : template.replace(/%s/g, chunk); + + return true +} + +/* + * To reduce build size, this file provides simple polyfills without + * overly excessive type checking and without modifying + * the global Array.prototype + * The polyfills are automatically removed in the commonjs build + * Also, only files in client/ & shared/ should use these functions + * files in server/ still use normal js function + */ + +function findIndex(array, predicate) { + var arguments$1 = arguments; + + if (!Array.prototype.findIndex) { + // idx needs to be a Number, for..in returns string + for (var idx = 0; idx < array.length; idx++) { + if (predicate.call(arguments$1[2], array[idx], idx, array)) { + return idx + } + } + return -1 + } + return array.findIndex(predicate, arguments[2]) +} + +function toArray(arg) { + if (!Array.from) { + return Array.prototype.slice.call(arg) + } + return Array.from(arg) +} + +function includes(array, value) { + if (!Array.prototype.includes) { + for (var idx in array) { + if (array[idx] === value) { + return true + } + } + + return false + } + return array.includes(value) +} + +var serverSequences = [ + [/&/g, '&'], + [//g, '>'], + [/"/g, '"'], + [/'/g, '''] +]; + +var clientSequences = [ + [/&/g, '\u0026'], + [//g, '\u003e'], + [/"/g, '\u0022'], + [/'/g, '\u0027'] +]; + +// sanitizes potentially dangerous characters +function escape(info, options, escapeOptions) { + var tagIDKeyName = options.tagIDKeyName; + var doEscape = escapeOptions.doEscape; if ( doEscape === void 0 ) doEscape = function (v) { return v; }; + var escaped = {}; + + for (var key in info) { + var value = info[key]; + + // no need to escape configuration options + if (includes(metaInfoOptionKeys, key)) { + escaped[key] = value; + continue + } + + var disableKey = disableOptionKeys[0]; + if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) { + // this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers + escaped[key] = value; + continue + } + + var tagId = info[tagIDKeyName]; + if (tagId) { + disableKey = disableOptionKeys[1]; + + // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped + if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) { + escaped[key] = value; + continue + } + } + + if (isString(value)) { + escaped[key] = doEscape(value); + } else if (isArray(value)) { + escaped[key] = value.map(function (v) { + return isObject(v) + ? escape(v, options, escapeOptions) + : doEscape(v) + }); + } else if (isObject(value)) { + escaped[key] = escape(value, options, escapeOptions); + } else { + escaped[key] = value; + } + } + + return escaped +} + +function arrayMerge(ref, target, source) { + var component = ref.component; + var tagIDKeyName = ref.tagIDKeyName; + var metaTemplateKeyName = ref.metaTemplateKeyName; + var contentKeyName = ref.contentKeyName; + + // we concat the arrays without merging objects contained in, + // but we check for a `vmid` property on each object in the array + // using an O(1) lookup associative array exploit + var destination = []; + + target.forEach(function (targetItem, targetIndex) { + // no tagID so no need to check for duplicity + if (!targetItem[tagIDKeyName]) { + destination.push(targetItem); + return + } + + var sourceIndex = findIndex(source, function (item) { return item[tagIDKeyName] === targetItem[tagIDKeyName]; }); + var sourceItem = source[sourceIndex]; + + // source doesnt contain any duplicate vmid's, we can keep targetItem + if (sourceIndex === -1) { + destination.push(targetItem); + return + } + + // when sourceItem explictly defines contentKeyName or innerHTML as undefined, its + // an indication that we need to skip the default behaviour or child has preference over parent + // which means we keep the targetItem and ignore/remove the sourceItem + if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) || + (sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) { + destination.push(targetItem); + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem + + // if source specifies null as content then ignore both the target as the source + if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) { + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // now we only need to check if the target has a template to combine it with the source + var targetTemplate = targetItem[metaTemplateKeyName]; + if (!targetTemplate) { + return + } + + var sourceTemplate = sourceItem[metaTemplateKeyName]; + + if (!sourceTemplate) { + // use parent template and child content + applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, targetTemplate); + } else if (!sourceItem[contentKeyName]) { + // use child template and parent content + applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + } + }); + + return destination.concat(source) +} + +function merge(target, source, options) { + if ( options === void 0 ) options = {}; + + // remove properties explicitly set to false so child components can + // optionally _not_ overwrite the parents content + // (for array properties this is checked in arrayMerge) + if (source.hasOwnProperty('title') && source.title === undefined) { + delete source.title; + } + + metaInfoAttributeKeys.forEach(function (attrKey) { + if (!source[attrKey]) { + return + } + + for (var key in source[attrKey]) { + if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { + delete source[attrKey][key]; + } + } + }); + + return deepmerge(target, source, { + arrayMerge: function (t, s) { return arrayMerge(options, t, s); } + }) +} + +/** + * Returns the `opts.option` $option value of the given `opts.component`. + * If methods are encountered, they will be bound to the component context. + * If `opts.deep` is true, will recursively merge all child component + * `opts.option` $option values into the returned result. + * + * @param {Object} opts - options + * @param {Object} opts.component - Vue component to fetch option data from + * @param {Boolean} opts.deep - look for data in child components as well? + * @param {Function} opts.arrayMerge - how should arrays be merged? + * @param {String} opts.keyName - the name of the option to look for + * @param {Object} [result={}] - result so far + * @return {Object} result - final aggregated result + */ +function getComponentOption(options, component, result) { + if ( options === void 0 ) options = {}; + if ( result === void 0 ) result = {}; + + var keyName = options.keyName; + var metaTemplateKeyName = options.metaTemplateKeyName; + var tagIDKeyName = options.tagIDKeyName; + var $options = component.$options; + var $children = component.$children; + + if (component._inactive) { + return result + } + + // only collect option data if it exists + if ($options[keyName]) { + var data = $options[keyName]; + + // if option is a function, replace it with it's result + if (isFunction(data)) { + data = data.call(component); + } + + // ignore data if its not an object, then we keep our previous result + if (!isObject(data)) { + return result + } + + // merge with existing options + result = merge(result, data, options); + } + + // collect & aggregate child options if deep = true + if ($children.length) { + $children.forEach(function (childComponent) { + // check if the childComponent is in a branch + // return otherwise so we dont walk all component branches unnecessarily + if (!inMetaInfoBranch(childComponent)) { + return + } + + result = getComponentOption(options, childComponent, result); + }); + } + + if (metaTemplateKeyName && result.meta) { + // apply templates if needed + result.meta.forEach(function (metaObject) { return applyTemplate(options, metaObject); }); + + // remove meta items with duplicate vmid's + result.meta = result.meta.filter(function (metaItem, index, arr) { + return ( + // keep meta item if it doesnt has a vmid + !metaItem.hasOwnProperty(tagIDKeyName) || + // or if it's the first item in the array with this vmid + index === findIndex(arr, function (item) { return item[tagIDKeyName] === metaItem[tagIDKeyName]; }) + ) + }); + } + + return result +} + +/** + * Returns the correct meta info for the given component + * (child components will overwrite parent meta info) + * + * @param {Object} component - the Vue instance to get meta info from + * @return {Object} - returned meta info + */ +function getMetaInfo(options, component, escapeSequences) { + if ( options === void 0 ) options = {}; + if ( escapeSequences === void 0 ) escapeSequences = []; + + // collect & aggregate all metaInfo $options + var info = getComponentOption(options, component, defaultInfo); + + // Remove all "template" tags from meta + + // backup the title chunk in case user wants access to it + if (info.title) { + info.titleChunk = info.title; + } + + // replace title with populated template + if (info.titleTemplate && info.titleTemplate !== '%s') { + applyTemplate({ component: component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || ''); + } + + // convert base tag to an array so it can be handled the same way + // as the other tags + if (info.base) { + info.base = Object.keys(info.base).length ? [info.base] : []; + } + + var escapeOptions = { + doEscape: function (value) { return escapeSequences.reduce(function (val, ref) { + var v = ref[0]; + var r = ref[1]; + + return val.replace(v, r); + }, value); } + }; + + disableOptionKeys.forEach(function (disableKey, index) { + if (index === 0) { + ensureIsArray(info, disableKey); + } else if (index === 1) { + for (var key in info[disableKey]) { + ensureIsArray(info[disableKey], key); + } + } + + escapeOptions[disableKey] = info[disableKey]; + }); + + // begin sanitization + info = escape(info, options, escapeOptions); + + return info +} + +/** + * Updates the document's html tag attributes + * + * @param {Object} attrs - the new document html attributes + * @param {HTMLElement} tag - the HTMLElement tag to update with new attrs + */ +function updateAttribute(ref, attrs, tag) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + var vueMetaAttrString = tag.getAttribute(attribute); + var vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + var toRemove = toArray(vueMetaAttrs); + + var keepIndexes = []; + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + var value = includes(booleanHtmlAttributes, attr) + ? '' + : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]; + + tag.setAttribute(attr, value || ''); + + if (!includes(vueMetaAttrs, attr)) { + vueMetaAttrs.push(attr); + } + + // filter below wont ever check -1 + keepIndexes.push(toRemove.indexOf(attr)); + } + } + + var removedAttributesCount = toRemove + .filter(function (el, index) { return !includes(keepIndexes, index); }) + .reduce(function (acc, attr) { + tag.removeAttribute(attr); + return acc + 1 + }, 0); + + if (vueMetaAttrs.length === removedAttributesCount) { + tag.removeAttribute(attribute); + } else { + tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(',')); + } +} + +/** + * Updates the document title + * + * @param {String} title - the new title of the document + */ +function updateTitle(title) { + if ( title === void 0 ) title = document.title; + + document.title = title; +} + +/** + * Updates meta tags inside and on the client. Borrowed from `react-helmet`: + * https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245 + * + * @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag + * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base + * @return {Object} - a representation of what tags changed + */ +function updateTag(appId, ref, type, tags, headTag, bodyTag) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + var tagIDKeyName = ref.tagIDKeyName; + + var oldHeadTags = toArray(headTag.querySelectorAll((type + "[" + attribute + "=\"" + appId + "\"], " + type + "[data-" + tagIDKeyName + "]"))); + var oldBodyTags = toArray(bodyTag.querySelectorAll((type + "[" + attribute + "=\"" + appId + "\"][data-body=\"true\"], " + type + "[data-" + tagIDKeyName + "][data-body=\"true\"]"))); + var dataAttributes = [tagIDKeyName, 'body']; + var newTags = []; + + if (tags.length > 1) { + // remove duplicates that could have been found by merging tags + // which include a mixin with metaInfo and that mixin is used + // by multiple components on the same page + var found = []; + tags = tags.filter(function (x) { + var k = JSON.stringify(x); + var res = !includes(found, k); + found.push(k); + return res + }); + } + + if (tags.length) { + tags.forEach(function (tag) { + var newElement = document.createElement(type); + + newElement.setAttribute(attribute, appId); + + var oldTags = tag.body !== true ? oldHeadTags : oldBodyTags; + + for (var attr in tag) { + if (tag.hasOwnProperty(attr)) { + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + } else if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + } else { + var _attr = includes(dataAttributes, attr) + ? ("data-" + attr) + : attr; + var value = isUndefined(tag[attr]) || includes(booleanHtmlAttributes, attr) ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + } + } + + // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + var indexToDelete; + var hasEqualElement = oldTags.some(function (existingTag, index) { + indexToDelete = index; + return newElement.isEqualNode(existingTag) + }); + + if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { + oldTags.splice(indexToDelete, 1); + } else { + newTags.push(newElement); + } + }); + } + + var oldTags = oldHeadTags.concat(oldBodyTags); + oldTags.forEach(function (tag) { return tag.parentNode.removeChild(tag); }); + newTags.forEach(function (tag) { + if (tag.getAttribute('data-body') === 'true') { + bodyTag.appendChild(tag); + } else { + headTag.appendChild(tag); + } + }); + + return { oldTags: oldTags, newTags: newTags } +} + +function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag] +} + +/** + * Performs client-side updates when new meta info is received + * + * @param {Object} newInfo - the meta info to update to + */ +function updateClientMetaInfo(appId, options, newInfo) { + if ( options === void 0 ) options = {}; + + var ssrAttribute = options.ssrAttribute; + + // only cache tags for current update + var tags = {}; + + var htmlTag = getTag(tags, 'html'); + + // if this is a server render, then dont update + if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + // remove the server render attribute so we can update on (next) changes + htmlTag.removeAttribute(ssrAttribute); + return false + } + + // initialize tracked changes + var addedTags = {}; + var removedTags = {}; + + for (var type in newInfo) { + // ignore these + if (includes(metaInfoOptionKeys, type)) { + continue + } + + if (type === 'title') { + // update the title + updateTitle(newInfo.title); + continue + } + + if (includes(metaInfoAttributeKeys, type)) { + var tagName = type.substr(0, 4); + updateAttribute(options, newInfo[type], getTag(tags, tagName)); + continue + } + + // tags should always be an array, ignore if it isnt + if (!isArray(newInfo[type])) { + continue + } + + var ref = updateTag( + appId, + options, + type, + newInfo[type], + getTag(tags, 'head'), + getTag(tags, 'body') + ); + var oldTags = ref.oldTags; + var newTags = ref.newTags; + + if (newTags.length) { + addedTags[type] = newTags; + removedTags[type] = oldTags; + } + } + + return { addedTags: addedTags, removedTags: removedTags } +} + +function _refresh(options) { + if ( options === void 0 ) options = {}; + + /** + * When called, will update the current meta info with new meta info. + * Useful when updating meta info as the result of an asynchronous + * action that resolves after the initial render takes place. + * + * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion + * to implement this method. + * + * @return {Object} - new meta info + */ + return function refresh() { + var metaInfo = getMetaInfo(options, this.$root, clientSequences); + + var appId = this.$root._vueMeta.appId; + var tags = updateClientMetaInfo(appId, options, metaInfo); + // emit "event" with new info + if (tags && isFunction(metaInfo.changed)) { + metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags); + } + + return { vm: this, metaInfo: metaInfo, tags: tags } + } +} + +/** + * Generates tag attributes for use on the server. + * + * @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate + * @param {Object} data - the attributes to generate + * @return {Object} - the attribute generator + */ +function attributeGenerator(ref, type, data) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + return { + text: function text() { + var attributeStr = ''; + var watchedAttrs = []; + + for (var attr in data) { + if (data.hasOwnProperty(attr)) { + watchedAttrs.push(attr); + + attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) + ? attr + : (attr + "=\"" + (isArray(data[attr]) ? data[attr].join(' ') : data[attr]) + "\""); + + attributeStr += ' '; + } + } + + attributeStr += attribute + "=\"" + ((watchedAttrs.sort()).join(',')) + "\""; + return attributeStr + } + } +} + +/** + * Generates title output for the server + * + * @param {'title'} type - the string "title" + * @param {String} data - the title text + * @return {Object} - the title generator + */ +function titleGenerator(appId, ref, type, data) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + return { + text: function text() { + return ("<" + type + ">" + data + "") + } + } +} + +/** + * Generates meta, base, link, style, script, noscript tags for use on the server + * + * @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag + * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base + * @return {Object} - the tag generator + */ +function tagGenerator(appId, ref, type, tags) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + var tagIDKeyName = ref.tagIDKeyName; + + return { + text: function text(ref) { + if ( ref === void 0 ) ref = {}; + var body = ref.body; if ( body === void 0 ) body = false; + + // build a string containing all tags of this type + return tags.reduce(function (tagsStr, tag) { + var tagKeys = Object.keys(tag); + + if (tagKeys.length === 0) { + return tagsStr // Bail on empty tag object + } + + if (Boolean(tag.body) !== body) { + return tagsStr + } + + // build a string containing all attributes of this tag + var attrs = tagKeys.reduce(function (attrsStr, attr) { + // these attributes are treated as children on the tag + if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') { + return attrsStr + } + + // these form the attribute list for this tag + var prefix = ''; + if ([tagIDKeyName, 'body'].includes(attr)) { + prefix = 'data-'; + } + + return isUndefined(tag[attr]) || booleanHtmlAttributes.includes(attr) + ? (attrsStr + " " + prefix + attr) + : (attrsStr + " " + prefix + attr + "=\"" + (tag[attr]) + "\"") + }, ''); + + // grab child content from one of these attributes, if possible + var content = tag.innerHTML || tag.cssText || ''; + + // generate tag exactly without any other redundant attribute + var observeTag = tag.once + ? '' + : (attribute + "=\"" + appId + "\""); + + // these tags have no end tag + var hasEndTag = !tagsWithoutEndTag.includes(type); + + // these tag types will have content inserted + var hasContent = hasEndTag && tagsWithInnerContent.includes(type); + + // the final string for this specific tag + return !hasContent + ? (tagsStr + "<" + type + " " + observeTag + attrs + (hasEndTag ? '/' : '') + ">") + : (tagsStr + "<" + type + " " + observeTag + attrs + ">" + content + "") + }, '') + } + } +} + +/** + * Converts a meta info property to one that can be stringified on the server + * + * @param {String} type - the type of data to convert + * @param {(String|Object|Array)} data - the data value + * @return {Object} - the new injector + */ + +function generateServerInjector(appId, options, type, data) { + if (type === 'title') { + return titleGenerator(appId, options, type, data) + } + + if (metaInfoAttributeKeys.includes(type)) { + return attributeGenerator(options, type, data) + } + + return tagGenerator(appId, options, type, data) +} + +function _inject(options) { + if ( options === void 0 ) options = {}; + + /** + * Converts the state of the meta info object such that each item + * can be compiled to a tag string on the server + * + * @this {Object} - Vue instance - ideally the root component + * @return {Object} - server meta info with `toString` methods + */ + return function inject() { + // get meta info with sensible defaults + var metaInfo = getMetaInfo(options, this.$root, serverSequences); + + // generate server injectors + for (var key in metaInfo) { + if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) { + metaInfo[key] = generateServerInjector('ssr', options, key, metaInfo[key]); + } + } + + return metaInfo + } +} + +function _$meta(options) { + if ( options === void 0 ) options = {}; + + var _refresh$1 = _refresh(options); + var _inject$1 = _inject(options); + + /** + * Returns an injector for server-side rendering. + * @this {Object} - the Vue instance (a root component) + * @return {Object} - injector + */ + return function $meta() { + return { + getOptions: function () { return getOptions(options); }, + refresh: _refresh$1.bind(this), + inject: _inject$1.bind(this), + pause: pause.bind(this), + resume: resume.bind(this) + } + } +} + +/** + * Plugin install function. + * @param {Function} Vue - the Vue constructor. + */ +function install(Vue, options) { + if ( options === void 0 ) options = {}; + + if (Vue.__vuemeta_installed) { + return + } + Vue.__vuemeta_installed = true; + + options = setOptions(options); + + Vue.prototype.$meta = _$meta(options); + + Vue.mixin(createMixin(Vue, options)); +} + +var index = { + version: version, + install: install, + hasMetaInfo: hasMetaInfo +}; + +export default index; diff --git a/dist/vue-meta.js b/dist/vue-meta.js new file mode 100644 index 0000000..840bd71 --- /dev/null +++ b/dist/vue-meta.js @@ -0,0 +1,1243 @@ +/** + * vue-meta v2.0.3 + * (c) 2019 + * - Declan de Wet + * - Sébastien Chopin (@Atinux) + * - All the amazing contributors + * @license MIT + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.VueMeta = factory()); +}(this, function () { 'use strict'; + + var version = "2.0.3"; + + // store an id to keep track of DOM updates + var batchId = null; + + function triggerUpdate(vm, hookName) { + // if an update was triggered during initialization or when an update was triggered by the + // metaInfo watcher, set initialized to null + // then we keep falsy value but know we need to run a triggerUpdate after initialization + if (!vm.$root._vueMeta.initialized && (vm.$root._vueMeta.initializing || hookName === 'watcher')) { + vm.$root._vueMeta.initialized = null; + } + + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { + // batch potential DOM updates to prevent extraneous re-rendering + batchUpdate(function () { return vm.$meta().refresh(); }); + } + } + + /** + * Performs a batched update. + * + * @param {(null|Number)} id - the ID of this update + * @param {Function} callback - the update to perform + * @return {Number} id - a new ID + */ + function batchUpdate(callback, timeout) { + if ( timeout === void 0 ) timeout = 10; + + clearTimeout(batchId); + + batchId = setTimeout(function () { + callback(); + }, timeout); + + return batchId + } + + /** + * checks if passed argument is an array + * @param {any} arg - the object to check + * @return {Boolean} - true if `arg` is an array + */ + function isArray(arg) { + return Array.isArray(arg) + } + + function isUndefined(arg) { + return typeof arg === 'undefined' + } + + function isObject(arg) { + return typeof arg === 'object' + } + + function isFunction(arg) { + return typeof arg === 'function' + } + + function isString(arg) { + return typeof arg === 'string' + } + + function ensureIsArray(arg, key) { + if (!key || !isObject(arg)) { + return isArray(arg) ? arg : [] + } + + if (!isArray(arg[key])) { + arg[key] = []; + } + return arg + } + + function ensuredPush(object, key, el) { + ensureIsArray(object, key); + + object[key].push(el); + } + + // Vue $root instance has a _vueMeta object property, otherwise its a boolean true + function hasMetaInfo(vm) { + if ( vm === void 0 ) vm = this; + + return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) + } + + // a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has + function inMetaInfoBranch(vm) { + if ( vm === void 0 ) vm = this; + + return vm && !isUndefined(vm._vueMeta) + } + + function addNavGuards(vm) { + // return when nav guards already added or no router exists + if (vm.$root._vueMeta.navGuards || !vm.$root.$router) { + /* istanbul ignore next */ + return + } + + vm.$root._vueMeta.navGuards = true; + + var $router = vm.$root.$router; + var $meta = vm.$root.$meta(); + + $router.beforeEach(function (to, from, next) { + $meta.pause(); + next(); + }); + + $router.afterEach(function () { + var ref = $meta.resume(); + var metaInfo = ref.metaInfo; + if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) { + metaInfo.afterNavigation(metaInfo); + } + }); + } + + var appId = 1; + + function createMixin(Vue, options) { + // for which Vue lifecycle hooks should the metaInfo be refreshed + var updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + + // watch for client side component updates + return { + beforeCreate: function beforeCreate() { + var this$1 = this; + + Object.defineProperty(this, '_hasMetaInfo', { + configurable: true, + get: function get() { + // Show deprecation warning once when devtools enabled + if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) { + console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); // eslint-disable-line no-console + this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; + } + return hasMetaInfo(this) + } + }); + + // Add a marker to know if it uses metaInfo + // _vnode is used to know that it's attached to a real component + // useful if we use some mixin to add some meta tags (like nuxt-i18n) + if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) { + if (!this.$root._vueMeta) { + this.$root._vueMeta = { appId: appId }; + appId++; + } + + // to speed up updates we keep track of branches which have a component with vue-meta info defined + // if _vueMeta = true it has info, if _vueMeta = false a child has info + if (!this._vueMeta) { + this._vueMeta = true; + + var p = this.$parent; + while (p && p !== this.$root) { + if (isUndefined(p._vueMeta)) { + p._vueMeta = false; + } + p = p.$parent; + } + } + + // coerce function-style metaInfo to a computed prop so we can observe + // it on creation + if (isFunction(this.$options[options.keyName])) { + if (!this.$options.computed) { + this.$options.computed = {}; + } + this.$options.computed.$metaInfo = this.$options[options.keyName]; + + if (!this.$isServer) { + // if computed $metaInfo exists, watch it for updates & trigger a refresh + // when it changes (i.e. automatically handle async actions that affect metaInfo) + // credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux) + ensuredPush(this.$options, 'created', function () { + this$1.$watch('$metaInfo', function () { + triggerUpdate(this, 'watcher'); + }); + }); + } + } + + // force an initial refresh on page load and prevent other lifecycleHooks + // to triggerUpdate until this initial refresh is finished + // this is to make sure that when a page is opened in an inactive tab which + // has throttled rAF/timers we still immediately set the page title + if (isUndefined(this.$root._vueMeta.initialized)) { + this.$root._vueMeta.initialized = this.$isServer; + + if (!this.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'beforeMount', function () { + // if this Vue-app was server rendered, set the appId to 'ssr' + // only one SSR app per page is supported + if (this$1.$root.$el && this$1.$root.$el.hasAttribute('data-server-rendered')) { + this$1.$root._vueMeta.appId = 'ssr'; + } + }); + + // we use the mounted hook here as on page load + ensuredPush(this.$options, 'mounted', function () { + if (!this$1.$root._vueMeta.initialized) { + // used in triggerUpdate to check if a change was triggered + // during initialization + this$1.$root._vueMeta.initializing = true; + + // refresh meta in nextTick so all child components have loaded + this$1.$nextTick(function () { + var this$1 = this; + + var ref = this.$root.$meta().refresh(); + var tags = ref.tags; + var metaInfo = ref.metaInfo; + + // After ssr hydration (identifier by tags === false) check + // if initialized was set to null in triggerUpdate. That'd mean + // that during initilazation changes where triggered which need + // to be applied OR a metaInfo watcher was triggered before the + // current hook was called + // (during initialization all changes are blocked) + if (tags === false && this.$root._vueMeta.initialized === null) { + this.$nextTick(function () { return triggerUpdate(this$1, 'initializing'); }); + } + + this.$root._vueMeta.initialized = true; + delete this.$root._vueMeta.initializing; + + // add the navigation guards if they havent been added yet + // they are needed for the afterNavigation callback + if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) { + addNavGuards(this); + } + }); + } + }); + + // add the navigation guards if requested + if (options.refreshOnceOnNavigation) { + addNavGuards(this); + } + } + } + + // do not trigger refresh on the server side + if (!this.$isServer) { + // no need to add this hooks on server side + updateOnLifecycleHook.forEach(function (lifecycleHook) { + ensuredPush(this$1.$options, lifecycleHook, function () { return triggerUpdate(this$1, lifecycleHook); }); + }); + + // re-render meta data when returning from a child component to parent + ensuredPush(this.$options, 'destroyed', function () { + // Wait that element is hidden before refreshing meta tags (to support animations) + var interval = setInterval(function () { + if (this$1.$el && this$1.$el.offsetParent !== null) { + /* istanbul ignore next line */ + return + } + + clearInterval(interval); + + if (!this$1.$parent) { + /* istanbul ignore next line */ + return + } + + triggerUpdate(this$1, 'destroyed'); + }, 50); + }); + } + } + } + } + } + + /** + * These are constant variables used throughout the application. + */ + + // set some sane defaults + var defaultInfo = { + title: '', + titleChunk: '', + titleTemplate: '%s', + htmlAttrs: {}, + bodyAttrs: {}, + headAttrs: {}, + base: [], + link: [], + meta: [], + style: [], + script: [], + noscript: [], + __dangerouslyDisableSanitizers: [], + __dangerouslyDisableSanitizersByTagID: {} + }; + + // This is the name of the component option that contains all the information that + // gets converted to the various meta tags & attributes for the page. + var keyName = 'metaInfo'; + + // This is the attribute vue-meta arguments on elements to know which it should + // manage and which it should ignore. + var attribute = 'data-vue-meta'; + + // This is the attribute that goes on the `html` tag to inform `vue-meta` + // that the server has already generated the meta tags for the initial render. + var ssrAttribute = 'data-vue-meta-server-rendered'; + + // This is the property that tells vue-meta to overwrite (instead of append) + // an item in a tag list. For example, if you have two `meta` tag list items + // that both have `vmid` of "description", then vue-meta will overwrite the + // shallowest one with the deepest one. + var tagIDKeyName = 'vmid'; + + // This is the key name for possible meta templates + var metaTemplateKeyName = 'template'; + + // This is the key name for the content-holding property + var contentKeyName = 'content'; + + var defaultOptions = { + keyName: keyName, + attribute: attribute, + ssrAttribute: ssrAttribute, + tagIDKeyName: tagIDKeyName, + contentKeyName: contentKeyName, + metaTemplateKeyName: metaTemplateKeyName + }; + + // List of metaInfo property keys which are configuration options (and dont generate html) + var metaInfoOptionKeys = [ + 'titleChunk', + 'titleTemplate', + 'changed', + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' + ]; + + // The metaInfo property keys which are used to disable escaping + var disableOptionKeys = [ + '__dangerouslyDisableSanitizers', + '__dangerouslyDisableSanitizersByTagID' + ]; + + // List of metaInfo property keys which only generates attributes and no tags + var metaInfoAttributeKeys = [ + 'htmlAttrs', + 'headAttrs', + 'bodyAttrs' + ]; + + // from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202 + var booleanHtmlAttributes = [ + 'allowfullscreen', + 'amp', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'compact', + 'controls', + 'declare', + 'default', + 'defaultchecked', + 'defaultmuted', + 'defaultselected', + 'defer', + 'disabled', + 'enabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'inert', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nohref', + 'noresize', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'pauseonexit', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'sortable', + 'truespeed', + 'typemustmatch', + 'visible' + ]; + + // eslint-disable-next-line no-console + var showWarningNotSupported = function () { return console.warn('This vue app/component has no vue-meta configuration'); }; + + function setOptions(options) { + // combine options + options = isObject(options) ? options : {}; + + for (var key in defaultOptions) { + if (!options[key]) { + options[key] = defaultOptions[key]; + } + } + + return options + } + + function getOptions(options) { + var optionsCopy = {}; + for (var key in options) { + optionsCopy[key] = options[key]; + } + return optionsCopy + } + + function pause(refresh) { + if ( refresh === void 0 ) refresh = true; + + this.$root._vueMeta.paused = true; + + return function () { return resume(refresh); } + } + + function resume(refresh) { + if ( refresh === void 0 ) refresh = true; + + this.$root._vueMeta.paused = false; + + if (refresh) { + return this.$root.$meta().refresh() + } + } + + function applyTemplate(ref, headObject, template, chunk) { + var component = ref.component; + var metaTemplateKeyName = ref.metaTemplateKeyName; + var contentKeyName = ref.contentKeyName; + + if (isUndefined(template)) { + template = headObject[metaTemplateKeyName]; + delete headObject[metaTemplateKeyName]; + } + + // return early if no template defined + if (!template) { + return false + } + + if (isUndefined(chunk)) { + chunk = headObject[contentKeyName]; + } + + headObject[contentKeyName] = isFunction(template) + ? template.call(component, chunk) + : template.replace(/%s/g, chunk); + + return true + } + + /* + * To reduce build size, this file provides simple polyfills without + * overly excessive type checking and without modifying + * the global Array.prototype + * The polyfills are automatically removed in the commonjs build + * Also, only files in client/ & shared/ should use these functions + * files in server/ still use normal js function + */ + + function findIndex(array, predicate) { + var arguments$1 = arguments; + + if (!Array.prototype.findIndex) { + // idx needs to be a Number, for..in returns string + for (var idx = 0; idx < array.length; idx++) { + if (predicate.call(arguments$1[2], array[idx], idx, array)) { + return idx + } + } + return -1 + } + return array.findIndex(predicate, arguments[2]) + } + + function toArray(arg) { + if (!Array.from) { + return Array.prototype.slice.call(arg) + } + return Array.from(arg) + } + + function includes(array, value) { + if (!Array.prototype.includes) { + for (var idx in array) { + if (array[idx] === value) { + return true + } + } + + return false + } + return array.includes(value) + } + + var clientSequences = [ + [/&/g, '\u0026'], + [//g, '\u003e'], + [/"/g, '\u0022'], + [/'/g, '\u0027'] + ]; + + // sanitizes potentially dangerous characters + function escape(info, options, escapeOptions) { + var tagIDKeyName = options.tagIDKeyName; + var doEscape = escapeOptions.doEscape; if ( doEscape === void 0 ) doEscape = function (v) { return v; }; + var escaped = {}; + + for (var key in info) { + var value = info[key]; + + // no need to escape configuration options + if (includes(metaInfoOptionKeys, key)) { + escaped[key] = value; + continue + } + + var disableKey = disableOptionKeys[0]; + if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) { + // this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers + escaped[key] = value; + continue + } + + var tagId = info[tagIDKeyName]; + if (tagId) { + disableKey = disableOptionKeys[1]; + + // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped + if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) { + escaped[key] = value; + continue + } + } + + if (isString(value)) { + escaped[key] = doEscape(value); + } else if (isArray(value)) { + escaped[key] = value.map(function (v) { + return isObject(v) + ? escape(v, options, escapeOptions) + : doEscape(v) + }); + } else if (isObject(value)) { + escaped[key] = escape(value, options, escapeOptions); + } else { + escaped[key] = value; + } + } + + return escaped + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var umd = createCommonjsModule(function (module, exports) { + (function (global, factory) { + module.exports = factory(); + }(commonjsGlobal, (function () { + var isMergeableObject = function isMergeableObject(value) { + return isNonNullObject(value) + && !isSpecial(value) + }; + + function isNonNullObject(value) { + return !!value && typeof value === 'object' + } + + function isSpecial(value) { + var stringValue = Object.prototype.toString.call(value); + + return stringValue === '[object RegExp]' + || stringValue === '[object Date]' + || isReactElement(value) + } + + // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 + var canUseSymbol = typeof Symbol === 'function' && Symbol.for; + var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; + + function isReactElement(value) { + return value.$$typeof === REACT_ELEMENT_TYPE + } + + function emptyTarget(val) { + return Array.isArray(val) ? [] : {} + } + + function cloneUnlessOtherwiseSpecified(value, options) { + return (options.clone !== false && options.isMergeableObject(value)) + ? deepmerge(emptyTarget(value), value, options) + : value + } + + function defaultArrayMerge(target, source, options) { + return target.concat(source).map(function(element) { + return cloneUnlessOtherwiseSpecified(element, options) + }) + } + + function getMergeFunction(key, options) { + if (!options.customMerge) { + return deepmerge + } + var customMerge = options.customMerge(key); + return typeof customMerge === 'function' ? customMerge : deepmerge + } + + function mergeObject(target, source, options) { + var destination = {}; + if (options.isMergeableObject(target)) { + Object.keys(target).forEach(function(key) { + destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); + }); + } + Object.keys(source).forEach(function(key) { + if (!options.isMergeableObject(source[key]) || !target[key]) { + destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); + } else { + destination[key] = getMergeFunction(key, options)(target[key], source[key], options); + } + }); + return destination + } + + function deepmerge(target, source, options) { + options = options || {}; + options.arrayMerge = options.arrayMerge || defaultArrayMerge; + options.isMergeableObject = options.isMergeableObject || isMergeableObject; + + var sourceIsArray = Array.isArray(source); + var targetIsArray = Array.isArray(target); + var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; + + if (!sourceAndTargetTypesMatch) { + return cloneUnlessOtherwiseSpecified(source, options) + } else if (sourceIsArray) { + return options.arrayMerge(target, source, options) + } else { + return mergeObject(target, source, options) + } + } + + deepmerge.all = function deepmergeAll(array, options) { + if (!Array.isArray(array)) { + throw new Error('first argument should be an array') + } + + return array.reduce(function(prev, next) { + return deepmerge(prev, next, options) + }, {}) + }; + + var deepmerge_1 = deepmerge; + + return deepmerge_1; + + }))); + }); + + function arrayMerge(ref, target, source) { + var component = ref.component; + var tagIDKeyName = ref.tagIDKeyName; + var metaTemplateKeyName = ref.metaTemplateKeyName; + var contentKeyName = ref.contentKeyName; + + // we concat the arrays without merging objects contained in, + // but we check for a `vmid` property on each object in the array + // using an O(1) lookup associative array exploit + var destination = []; + + target.forEach(function (targetItem, targetIndex) { + // no tagID so no need to check for duplicity + if (!targetItem[tagIDKeyName]) { + destination.push(targetItem); + return + } + + var sourceIndex = findIndex(source, function (item) { return item[tagIDKeyName] === targetItem[tagIDKeyName]; }); + var sourceItem = source[sourceIndex]; + + // source doesnt contain any duplicate vmid's, we can keep targetItem + if (sourceIndex === -1) { + destination.push(targetItem); + return + } + + // when sourceItem explictly defines contentKeyName or innerHTML as undefined, its + // an indication that we need to skip the default behaviour or child has preference over parent + // which means we keep the targetItem and ignore/remove the sourceItem + if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) || + (sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) { + destination.push(targetItem); + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem + + // if source specifies null as content then ignore both the target as the source + if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) { + // remove current index from source array so its not concatenated to destination below + source.splice(sourceIndex, 1); + return + } + + // now we only need to check if the target has a template to combine it with the source + var targetTemplate = targetItem[metaTemplateKeyName]; + if (!targetTemplate) { + return + } + + var sourceTemplate = sourceItem[metaTemplateKeyName]; + + if (!sourceTemplate) { + // use parent template and child content + applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, targetTemplate); + } else if (!sourceItem[contentKeyName]) { + // use child template and parent content + applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + } + }); + + return destination.concat(source) + } + + function merge(target, source, options) { + if ( options === void 0 ) options = {}; + + // remove properties explicitly set to false so child components can + // optionally _not_ overwrite the parents content + // (for array properties this is checked in arrayMerge) + if (source.hasOwnProperty('title') && source.title === undefined) { + delete source.title; + } + + metaInfoAttributeKeys.forEach(function (attrKey) { + if (!source[attrKey]) { + return + } + + for (var key in source[attrKey]) { + if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { + delete source[attrKey][key]; + } + } + }); + + return umd(target, source, { + arrayMerge: function (t, s) { return arrayMerge(options, t, s); } + }) + } + + /** + * Returns the `opts.option` $option value of the given `opts.component`. + * If methods are encountered, they will be bound to the component context. + * If `opts.deep` is true, will recursively merge all child component + * `opts.option` $option values into the returned result. + * + * @param {Object} opts - options + * @param {Object} opts.component - Vue component to fetch option data from + * @param {Boolean} opts.deep - look for data in child components as well? + * @param {Function} opts.arrayMerge - how should arrays be merged? + * @param {String} opts.keyName - the name of the option to look for + * @param {Object} [result={}] - result so far + * @return {Object} result - final aggregated result + */ + function getComponentOption(options, component, result) { + if ( options === void 0 ) options = {}; + if ( result === void 0 ) result = {}; + + var keyName = options.keyName; + var metaTemplateKeyName = options.metaTemplateKeyName; + var tagIDKeyName = options.tagIDKeyName; + var $options = component.$options; + var $children = component.$children; + + if (component._inactive) { + return result + } + + // only collect option data if it exists + if ($options[keyName]) { + var data = $options[keyName]; + + // if option is a function, replace it with it's result + if (isFunction(data)) { + data = data.call(component); + } + + // ignore data if its not an object, then we keep our previous result + if (!isObject(data)) { + return result + } + + // merge with existing options + result = merge(result, data, options); + } + + // collect & aggregate child options if deep = true + if ($children.length) { + $children.forEach(function (childComponent) { + // check if the childComponent is in a branch + // return otherwise so we dont walk all component branches unnecessarily + if (!inMetaInfoBranch(childComponent)) { + return + } + + result = getComponentOption(options, childComponent, result); + }); + } + + if (metaTemplateKeyName && result.meta) { + // apply templates if needed + result.meta.forEach(function (metaObject) { return applyTemplate(options, metaObject); }); + + // remove meta items with duplicate vmid's + result.meta = result.meta.filter(function (metaItem, index, arr) { + return ( + // keep meta item if it doesnt has a vmid + !metaItem.hasOwnProperty(tagIDKeyName) || + // or if it's the first item in the array with this vmid + index === findIndex(arr, function (item) { return item[tagIDKeyName] === metaItem[tagIDKeyName]; }) + ) + }); + } + + return result + } + + /** + * Returns the correct meta info for the given component + * (child components will overwrite parent meta info) + * + * @param {Object} component - the Vue instance to get meta info from + * @return {Object} - returned meta info + */ + function getMetaInfo(options, component, escapeSequences) { + if ( options === void 0 ) options = {}; + if ( escapeSequences === void 0 ) escapeSequences = []; + + // collect & aggregate all metaInfo $options + var info = getComponentOption(options, component, defaultInfo); + + // Remove all "template" tags from meta + + // backup the title chunk in case user wants access to it + if (info.title) { + info.titleChunk = info.title; + } + + // replace title with populated template + if (info.titleTemplate && info.titleTemplate !== '%s') { + applyTemplate({ component: component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || ''); + } + + // convert base tag to an array so it can be handled the same way + // as the other tags + if (info.base) { + info.base = Object.keys(info.base).length ? [info.base] : []; + } + + var escapeOptions = { + doEscape: function (value) { return escapeSequences.reduce(function (val, ref) { + var v = ref[0]; + var r = ref[1]; + + return val.replace(v, r); + }, value); } + }; + + disableOptionKeys.forEach(function (disableKey, index) { + if (index === 0) { + ensureIsArray(info, disableKey); + } else if (index === 1) { + for (var key in info[disableKey]) { + ensureIsArray(info[disableKey], key); + } + } + + escapeOptions[disableKey] = info[disableKey]; + }); + + // begin sanitization + info = escape(info, options, escapeOptions); + + return info + } + + /** + * Updates the document's html tag attributes + * + * @param {Object} attrs - the new document html attributes + * @param {HTMLElement} tag - the HTMLElement tag to update with new attrs + */ + function updateAttribute(ref, attrs, tag) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + + var vueMetaAttrString = tag.getAttribute(attribute); + var vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + var toRemove = toArray(vueMetaAttrs); + + var keepIndexes = []; + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + var value = includes(booleanHtmlAttributes, attr) + ? '' + : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]; + + tag.setAttribute(attr, value || ''); + + if (!includes(vueMetaAttrs, attr)) { + vueMetaAttrs.push(attr); + } + + // filter below wont ever check -1 + keepIndexes.push(toRemove.indexOf(attr)); + } + } + + var removedAttributesCount = toRemove + .filter(function (el, index) { return !includes(keepIndexes, index); }) + .reduce(function (acc, attr) { + tag.removeAttribute(attr); + return acc + 1 + }, 0); + + if (vueMetaAttrs.length === removedAttributesCount) { + tag.removeAttribute(attribute); + } else { + tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(',')); + } + } + + /** + * Updates the document title + * + * @param {String} title - the new title of the document + */ + function updateTitle(title) { + if ( title === void 0 ) title = document.title; + + document.title = title; + } + + /** + * Updates meta tags inside and on the client. Borrowed from `react-helmet`: + * https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245 + * + * @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag + * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base + * @return {Object} - a representation of what tags changed + */ + function updateTag(appId, ref, type, tags, headTag, bodyTag) { + if ( ref === void 0 ) ref = {}; + var attribute = ref.attribute; + var tagIDKeyName = ref.tagIDKeyName; + + var oldHeadTags = toArray(headTag.querySelectorAll((type + "[" + attribute + "=\"" + appId + "\"], " + type + "[data-" + tagIDKeyName + "]"))); + var oldBodyTags = toArray(bodyTag.querySelectorAll((type + "[" + attribute + "=\"" + appId + "\"][data-body=\"true\"], " + type + "[data-" + tagIDKeyName + "][data-body=\"true\"]"))); + var dataAttributes = [tagIDKeyName, 'body']; + var newTags = []; + + if (tags.length > 1) { + // remove duplicates that could have been found by merging tags + // which include a mixin with metaInfo and that mixin is used + // by multiple components on the same page + var found = []; + tags = tags.filter(function (x) { + var k = JSON.stringify(x); + var res = !includes(found, k); + found.push(k); + return res + }); + } + + if (tags.length) { + tags.forEach(function (tag) { + var newElement = document.createElement(type); + + newElement.setAttribute(attribute, appId); + + var oldTags = tag.body !== true ? oldHeadTags : oldBodyTags; + + for (var attr in tag) { + if (tag.hasOwnProperty(attr)) { + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + } else if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + } else { + var _attr = includes(dataAttributes, attr) + ? ("data-" + attr) + : attr; + var value = isUndefined(tag[attr]) || includes(booleanHtmlAttributes, attr) ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + } + } + + // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + var indexToDelete; + var hasEqualElement = oldTags.some(function (existingTag, index) { + indexToDelete = index; + return newElement.isEqualNode(existingTag) + }); + + if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { + oldTags.splice(indexToDelete, 1); + } else { + newTags.push(newElement); + } + }); + } + + var oldTags = oldHeadTags.concat(oldBodyTags); + oldTags.forEach(function (tag) { return tag.parentNode.removeChild(tag); }); + newTags.forEach(function (tag) { + if (tag.getAttribute('data-body') === 'true') { + bodyTag.appendChild(tag); + } else { + headTag.appendChild(tag); + } + }); + + return { oldTags: oldTags, newTags: newTags } + } + + function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag] + } + + /** + * Performs client-side updates when new meta info is received + * + * @param {Object} newInfo - the meta info to update to + */ + function updateClientMetaInfo(appId, options, newInfo) { + if ( options === void 0 ) options = {}; + + var ssrAttribute = options.ssrAttribute; + + // only cache tags for current update + var tags = {}; + + var htmlTag = getTag(tags, 'html'); + + // if this is a server render, then dont update + if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + // remove the server render attribute so we can update on (next) changes + htmlTag.removeAttribute(ssrAttribute); + return false + } + + // initialize tracked changes + var addedTags = {}; + var removedTags = {}; + + for (var type in newInfo) { + // ignore these + if (includes(metaInfoOptionKeys, type)) { + continue + } + + if (type === 'title') { + // update the title + updateTitle(newInfo.title); + continue + } + + if (includes(metaInfoAttributeKeys, type)) { + var tagName = type.substr(0, 4); + updateAttribute(options, newInfo[type], getTag(tags, tagName)); + continue + } + + // tags should always be an array, ignore if it isnt + if (!isArray(newInfo[type])) { + continue + } + + var ref = updateTag( + appId, + options, + type, + newInfo[type], + getTag(tags, 'head'), + getTag(tags, 'body') + ); + var oldTags = ref.oldTags; + var newTags = ref.newTags; + + if (newTags.length) { + addedTags[type] = newTags; + removedTags[type] = oldTags; + } + } + + return { addedTags: addedTags, removedTags: removedTags } + } + + function _refresh(options) { + if ( options === void 0 ) options = {}; + + /** + * When called, will update the current meta info with new meta info. + * Useful when updating meta info as the result of an asynchronous + * action that resolves after the initial render takes place. + * + * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion + * to implement this method. + * + * @return {Object} - new meta info + */ + return function refresh() { + var metaInfo = getMetaInfo(options, this.$root, clientSequences); + + var appId = this.$root._vueMeta.appId; + var tags = updateClientMetaInfo(appId, options, metaInfo); + // emit "event" with new info + if (tags && isFunction(metaInfo.changed)) { + metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags); + } + + return { vm: this, metaInfo: metaInfo, tags: tags } + } + } + + function _$meta(options) { + if ( options === void 0 ) options = {}; + + var _refresh$1 = _refresh(options); + var inject = function () {}; + + /** + * Returns an injector for server-side rendering. + * @this {Object} - the Vue instance (a root component) + * @return {Object} - injector + */ + return function $meta() { + if (!this.$root._vueMeta) { + return { + getOptions: showWarningNotSupported, + refresh: showWarningNotSupported, + inject: showWarningNotSupported, + pause: showWarningNotSupported, + resume: showWarningNotSupported + } + } + + return { + getOptions: function () { return getOptions(options); }, + refresh: _refresh$1.bind(this), + inject: inject, + pause: pause.bind(this), + resume: resume.bind(this) + } + } + } + + /** + * Plugin install function. + * @param {Function} Vue - the Vue constructor. + */ + function install(Vue, options) { + if ( options === void 0 ) options = {}; + + if (Vue.__vuemeta_installed) { + return + } + Vue.__vuemeta_installed = true; + + options = setOptions(options); + + Vue.prototype.$meta = _$meta(options); + + Vue.mixin(createMixin(Vue, options)); + } + + // automatic install + if (!isUndefined(window) && !isUndefined(window.Vue)) { + /* istanbul ignore next */ + install(window.Vue); + } + + var browser = { + version: version, + install: install, + hasMetaInfo: hasMetaInfo + }; + + return browser; + +})); diff --git a/dist/vue-meta.min.js b/dist/vue-meta.min.js new file mode 100644 index 0000000..5e8861e --- /dev/null +++ b/dist/vue-meta.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).VueMeta=t()}(this,function(){"use strict";var e=null;function t(t,n){t.$root._vueMeta.initialized||!t.$root._vueMeta.initializing&&"watcher"!==n||(t.$root._vueMeta.initialized=null),t.$root._vueMeta.initialized&&!t.$root._vueMeta.paused&&function(t,n){void 0===n&&(n=10);clearTimeout(e),e=setTimeout(function(){t()},n)}(function(){return t.$meta().refresh()})}function n(e){return Array.isArray(e)}function r(e){return void 0===e}function i(e){return"object"==typeof e}function o(e){return"function"==typeof e}function a(e,t){return t&&i(e)?(n(e[t])||(e[t]=[]),e):n(e)?e:[]}function u(e,t,n){a(e,t),e[t].push(n)}function s(e){return void 0===e&&(e=this),e&&(!0===e._vueMeta||i(e._vueMeta))}function f(e){if(!e.$root._vueMeta.navGuards&&e.$root.$router){e.$root._vueMeta.navGuards=!0;var t=e.$root.$router,n=e.$root.$meta();t.beforeEach(function(e,t,r){n.pause(),r()}),t.afterEach(function(){var e=n.resume().metaInfo;e&&e.afterNavigation&&o(e.afterNavigation)&&e.afterNavigation(e)})}}var c=1;var l={title:"",titleChunk:"",titleTemplate:"%s",htmlAttrs:{},bodyAttrs:{},headAttrs:{},base:[],link:[],meta:[],style:[],script:[],noscript:[],__dangerouslyDisableSanitizers:[],__dangerouslyDisableSanitizersByTagID:{}},d={keyName:"metaInfo",attribute:"data-vue-meta",ssrAttribute:"data-vue-meta-server-rendered",tagIDKeyName:"vmid",contentKeyName:"content",metaTemplateKeyName:"template"},v=["titleChunk","titleTemplate","changed","__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],h=["__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],p=["htmlAttrs","headAttrs","bodyAttrs"],m=["allowfullscreen","amp","async","autofocus","autoplay","checked","compact","controls","declare","default","defaultchecked","defaultmuted","defaultselected","defer","disabled","enabled","formnovalidate","hidden","indeterminate","inert","ismap","itemscope","loop","multiple","muted","nohref","noresize","noshade","novalidate","nowrap","open","pauseonexit","readonly","required","reversed","scoped","seamless","selected","sortable","truespeed","typemustmatch","visible"],y=function(){return console.warn("This vue app/component has no vue-meta configuration")};function g(e){return void 0===e&&(e=!0),this.$root._vueMeta.paused=!0,function(){return b(e)}}function b(e){if(void 0===e&&(e=!0),this.$root._vueMeta.paused=!1,e)return this.$root.$meta().refresh()}function $(e,t,n,i){var a=e.component,u=e.metaTemplateKeyName,s=e.contentKeyName;return r(n)&&(n=t[u],delete t[u]),!!n&&(r(i)&&(i=t[s]),t[s]=o(n)?n.call(a,i):n.replace(/%s/g,i),!0)}function M(e,t){var n=arguments;if(!Array.prototype.findIndex){for(var r=0;r/g,">"],[/"/g,'"'],[/'/g,"'"]];"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;var N=function(e,t){return e(t={exports:{}},t.exports),t.exports}(function(e,t){e.exports=function(){var e=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var n=Object.prototype.toString.call(e);return"[object RegExp]"===n||"[object Date]"===n||function(e){return e.$$typeof===t}(e)}(e)},t="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function n(e,t){return!1!==t.clone&&t.isMergeableObject(e)?o((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function r(e,t,r){return e.concat(t).map(function(e){return n(e,r)})}function i(e,t,r){var i={};return r.isMergeableObject(e)&&Object.keys(e).forEach(function(t){i[t]=n(e[t],r)}),Object.keys(t).forEach(function(a){r.isMergeableObject(t[a])&&e[a]?i[a]=function(e,t){if(!t.customMerge)return o;var n=t.customMerge(e);return"function"==typeof n?n:o}(a,r)(e[a],t[a],r):i[a]=n(t[a],r)}),i}function o(t,o,a){(a=a||{}).arrayMerge=a.arrayMerge||r,a.isMergeableObject=a.isMergeableObject||e;var u=Array.isArray(o),s=Array.isArray(t),f=u===s;return f?u?a.arrayMerge(t,o,a):i(t,o,a):n(o,a)}return o.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce(function(e,n){return o(e,n,t)},{})},o}()});function w(e,t,n){return void 0===n&&(n={}),t.hasOwnProperty("title")&&void 0===t.title&&delete t.title,p.forEach(function(e){if(t[e])for(var n in t[e])t[e].hasOwnProperty(n)&&void 0===t[e][n]&&delete t[e][n]}),N(e,t,{arrayMerge:function(e,t){return function(e,t,n){var r=e.component,i=e.tagIDKeyName,o=e.metaTemplateKeyName,a=e.contentKeyName,u=[];return t.forEach(function(e,t){if(e[i]){var s=M(n,function(t){return t[i]===e[i]}),f=n[s];if(-1!==s){if(f.hasOwnProperty(a)&&void 0===f[a]||f.hasOwnProperty("innerHTML")&&void 0===f.innerHTML)return u.push(e),void n.splice(s,1);if(null!==f[a]&&null!==f.innerHTML){var c=e[o];c&&(f[o]?f[a]||$({component:r,metaTemplateKeyName:o,contentKeyName:a},f,void 0,e[a]):$({component:r,metaTemplateKeyName:o,contentKeyName:a},f,c))}else n.splice(s,1)}else u.push(e)}else u.push(e)}),u.concat(n)}(n,e,t)}})}function I(e,t,n){void 0===e&&(e={}),void 0===n&&(n={});var a=e.keyName,u=e.metaTemplateKeyName,s=e.tagIDKeyName,f=t.$options,c=t.$children;if(t._inactive)return n;if(f[a]){var l=f[a];if(o(l)&&(l=l.call(t)),!i(l))return n;n=w(n,l,e)}return c.length&&c.forEach(function(t){(function(e){return void 0===e&&(e=this),e&&!r(e._vueMeta)})(t)&&(n=I(e,t,n))}),u&&n.meta&&(n.meta.forEach(function(t){return $(e,t)}),n.meta=n.meta.filter(function(e,t,n){return!e.hasOwnProperty(s)||t===M(n,function(t){return t[s]===e[s]})})),n}function O(e,t,r){void 0===e&&(e={}),void 0===r&&(r=[]);var o=I(e,t,l);o.title&&(o.titleChunk=o.title),o.titleTemplate&&"%s"!==o.titleTemplate&&$({component:t,contentKeyName:"title"},o,o.titleTemplate,o.titleChunk||""),o.base&&(o.base=Object.keys(o.base).length?[o.base]:[]);var u={doEscape:function(e){return r.reduce(function(e,t){var n=t[0],r=t[1];return e.replace(n,r)},e)}};return h.forEach(function(e,t){if(0===t)a(o,e);else if(1===t)for(var n in o[e])a(o[e],n);u[e]=o[e]}),o=function e(t,r,o){var a=r.tagIDKeyName,u=o.doEscape;void 0===u&&(u=function(e){return e});var s={};for(var f in t){var c=t[f];if(T(v,f))s[f]=c;else{var l=h[0];if(o[l]&&T(o[l],f))s[f]=c;else{var d=t[a];d&&(l=h[1],o[l]&&o[l][d]&&T(o[l][d],f))?s[f]=c:"string"==typeof c?s[f]=u(c):n(c)?s[f]=c.map(function(t){return i(t)?e(t,r,o):u(t)}):i(c)?s[f]=e(c,r,o):s[f]=c}}}return s}(o,e,u)}function z(e,t,r){void 0===e&&(e={});var i=e.attribute,o=r.getAttribute(i),a=o?o.split(","):[],u=_(a),s=[];for(var f in t)if(t.hasOwnProperty(f)){var c=T(m,f)?"":n(t[f])?t[f].join(" "):t[f];r.setAttribute(f,c||""),T(a,f)||a.push(f),s.push(u.indexOf(f))}var l=u.filter(function(e,t){return!T(s,t)}).reduce(function(e,t){return r.removeAttribute(t),e+1},0);a.length===l?r.removeAttribute(i):r.setAttribute(i,a.sort().join(","))}function E(e,t,n,i,o,a){void 0===t&&(t={});var u=t.attribute,s=t.tagIDKeyName,f=_(o.querySelectorAll(n+"["+u+'="'+e+'"], '+n+"[data-"+s+"]")),c=_(a.querySelectorAll(n+"["+u+'="'+e+'"][data-body="true"], '+n+"[data-"+s+'][data-body="true"]')),l=[s,"body"],d=[];if(i.length>1){var v=[];i=i.filter(function(e){var t=JSON.stringify(e),n=!T(v,t);return v.push(t),n})}i.length&&i.forEach(function(t){var i=document.createElement(n);i.setAttribute(u,e);var o,a=!0!==t.body?f:c;for(var s in t)if(t.hasOwnProperty(s))if("innerHTML"===s)i.innerHTML=t.innerHTML;else if("cssText"===s)i.styleSheet?i.styleSheet.cssText=t.cssText:i.appendChild(document.createTextNode(t.cssText));else{var v=T(l,s)?"data-"+s:s,h=r(t[s])||T(m,s)?"":t[s];i.setAttribute(v,h)}a.some(function(e,t){return o=t,i.isEqualNode(e)})&&(o||0===o)?a.splice(o,1):d.push(i)});var h=f.concat(c);return h.forEach(function(e){return e.parentNode.removeChild(e)}),d.forEach(function(e){"true"===e.getAttribute("data-body")?a.appendChild(e):o.appendChild(e)}),{oldTags:h,newTags:d}}function S(e,t){return e[t]||(e[t]=document.getElementsByTagName(t)[0]),e[t]}function j(e){return void 0===e&&(e={}),function(){var t=O(e,this.$root,A),r=function(e,t,r){void 0===t&&(t={});var i=t.ssrAttribute,o={},a=S(o,"html");if("ssr"===e&&a.hasAttribute(i))return a.removeAttribute(i),!1;var u,s={},f={};for(var c in r)if(!T(v,c))if("title"!==c){if(T(p,c)){var l=c.substr(0,4);z(t,r[c],S(o,l))}else if(n(r[c])){var d=E(e,t,c,r[c],S(o,"head"),S(o,"body")),h=d.oldTags,m=d.newTags;m.length&&(s[c]=m,f[c]=h)}}else void 0===(u=r.title)&&(u=document.title),document.title=u;return{addedTags:s,removedTags:f}}(this.$root._vueMeta.appId,e,t);return r&&o(t.changed)&&t.changed(t,r.addedTags,r.removedTags),{vm:this,metaInfo:t,tags:r}}}function x(e,n){void 0===n&&(n={}),e.__vuemeta_installed||(e.__vuemeta_installed=!0,n=function(e){for(var t in e=i(e)?e:{},d)e[t]||(e[t]=d[t]);return e}(n),e.prototype.$meta=function(e){void 0===e&&(e={});var t=j(e),n=function(){};return function(){return this.$root._vueMeta?{getOptions:function(){return function(e){var t={};for(var n in e)t[n]=e[n];return t}(e)},refresh:t.bind(this),inject:n,pause:g.bind(this),resume:b.bind(this)}:{getOptions:y,refresh:y,inject:y,pause:y,resume:y}}}(n),e.mixin(function(e,n){var i=["activated","deactivated","beforeMount"];return{beforeCreate:function(){var a=this;if(Object.defineProperty(this,"_hasMetaInfo",{configurable:!0,get:function(){return e.config.devtools&&!this.$root._vueMeta.hasMetaInfoDeprecationWarningShown&&(console.warn("VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead"),this.$root._vueMeta.hasMetaInfoDeprecationWarningShown=!0),s(this)}}),!r(this.$options[n.keyName])&&null!==this.$options[n.keyName]){if(this.$root._vueMeta||(this.$root._vueMeta={appId:c},c++),!this._vueMeta){this._vueMeta=!0;for(var l=this.$parent;l&&l!==this.$root;)r(l._vueMeta)&&(l._vueMeta=!1),l=l.$parent}o(this.$options[n.keyName])&&(this.$options.computed||(this.$options.computed={}),this.$options.computed.$metaInfo=this.$options[n.keyName],this.$isServer||u(this.$options,"created",function(){a.$watch("$metaInfo",function(){t(this,"watcher")})})),r(this.$root._vueMeta.initialized)&&(this.$root._vueMeta.initialized=this.$isServer,this.$root._vueMeta.initialized||(u(this.$options,"beforeMount",function(){a.$root.$el&&a.$root.$el.hasAttribute("data-server-rendered")&&(a.$root._vueMeta.appId="ssr")}),u(this.$options,"mounted",function(){a.$root._vueMeta.initialized||(a.$root._vueMeta.initializing=!0,a.$nextTick(function(){var e=this,r=this.$root.$meta().refresh(),i=r.tags,o=r.metaInfo;!1===i&&null===this.$root._vueMeta.initialized&&this.$nextTick(function(){return t(e,"initializing")}),this.$root._vueMeta.initialized=!0,delete this.$root._vueMeta.initializing,!n.refreshOnceOnNavigation&&o.afterNavigation&&f(this)}))}),n.refreshOnceOnNavigation&&f(this))),this.$isServer||(i.forEach(function(e){u(a.$options,e,function(){return t(a,e)})}),u(this.$options,"destroyed",function(){var e=setInterval(function(){a.$el&&null!==a.$el.offsetParent||(clearInterval(e),a.$parent&&t(a,"destroyed"))},50)}))}}}}(e,n)))}return r(window)||r(window.Vue)||x(window.Vue),{version:"2.0.3",install:x,hasMetaInfo:s}}); diff --git a/package.json b/package.json index 967ce8d..692fb7d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "lint": "eslint src test", "prerelease": "git checkout master && git pull -r", "release": "yarn lint && yarn test && yarn build && standard-version", - "postrelease": "yarn build", + "postrelease": "yarn build && git add dist/* && git commit --amend", "test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser", "test:e2e-ssr": "jest test/e2e/ssr", "test:e2e-browser": "jest test/e2e/browser",