diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a365e..9e288c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.1.0](https://github.com/nuxt/vue-meta/compare/v2.0.3...v2.1.0) (2019-07-24) + + +### Bug Fixes + +* add warning for v1 boolean attribute syntax ([bfeab17](https://github.com/nuxt/vue-meta/commit/bfeab17)) +* also use ssrAppId for client update ([50c0509](https://github.com/nuxt/vue-meta/commit/50c0509)) +* don't generate tag if metaInfo.title is null or false ([#409](https://github.com/nuxt/vue-meta/issues/409)) ([39ef287](https://github.com/nuxt/vue-meta/commit/39ef287)) +* dont change title when value is undefined (fix [#396](https://github.com/nuxt/vue-meta/issues/396)) ([90f9710](https://github.com/nuxt/vue-meta/commit/90f9710)) +* dont update title on client with falsy value except empty string ([6efcdf1](https://github.com/nuxt/vue-meta/commit/6efcdf1)) +* ensure hasAttribute exists on $root.$el ([f1511ac](https://github.com/nuxt/vue-meta/commit/f1511ac)) +* only show boolean attrs with truthy value ([1d9072a](https://github.com/nuxt/vue-meta/commit/1d9072a)) + + +### Features + +* add option for prepending (no)script to body ([#410](https://github.com/nuxt/vue-meta/issues/410)) ([05163a7](https://github.com/nuxt/vue-meta/commit/05163a7)) +* auto add ssrAttribute to htmlAttrs ([9cf6d32](https://github.com/nuxt/vue-meta/commit/9cf6d32)) +* enable onload callbacks ([#414](https://github.com/nuxt/vue-meta/issues/414)) ([fc71e1f](https://github.com/nuxt/vue-meta/commit/fc71e1f)) +* make ssr app id configurable ([b0c85e5](https://github.com/nuxt/vue-meta/commit/b0c85e5)) +* support json content (without disabling sanitizers) ([#415](https://github.com/nuxt/vue-meta/issues/415)) ([51fe6ea](https://github.com/nuxt/vue-meta/commit/51fe6ea)) + + +### Tests + +* enable all getMetaInfo tests again ([24d7fee](https://github.com/nuxt/vue-meta/commit/24d7fee)) +* update browser config ([8c35863](https://github.com/nuxt/vue-meta/commit/8c35863)) + + + ### [2.0.5](https://github.com/nuxt/vue-meta/compare/v2.0.3...v2.0.5) (2019-07-11) diff --git a/dist/vue-meta.common.js b/dist/vue-meta.common.js index e6c2e5b..f92adca 100644 --- a/dist/vue-meta.common.js +++ b/dist/vue-meta.common.js @@ -1,5 +1,5 @@ /** - * vue-meta v2.0.5 + * vue-meta v2.1.0 * (c) 2019 * - Declan de Wet * - Sébastien Chopin (@Atinux) @@ -13,12 +13,11 @@ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'defau var deepmerge = _interopDefault(require('deepmerge')); -var version = "2.0.5"; +var version = "2.1.0"; // store an id to keep track of DOM updates -var batchId = null; - -function triggerUpdate (vm, hookName) { +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 @@ -28,10 +27,9 @@ function triggerUpdate (vm, hookName) { if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { // batch potential DOM updates to prevent extraneous re-rendering - batchUpdate(function () { return vm.$meta().refresh(); }); + batchUpdate(() => vm.$meta().refresh()); } } - /** * Performs a batched update. * @@ -39,16 +37,13 @@ function triggerUpdate (vm, hookName) { * @param {Function} callback - the update to perform * @return {Number} id - a new ID */ -function batchUpdate (callback, timeout) { - if ( timeout === void 0 ) timeout = 10; +function batchUpdate(callback, timeout = 10) { clearTimeout(batchId); - - batchId = setTimeout(function () { + batchId = setTimeout(() => { callback(); }, timeout); - - return batchId + return batchId; } /** @@ -56,247 +51,254 @@ function batchUpdate (callback, timeout) { * @param {any} arg - the object to check * @return {Boolean} - true if `arg` is an array */ -function isArray (arg) { - return Array.isArray(arg) +function isArray(arg) { + return Array.isArray(arg); +} +function isUndefined(arg) { + return typeof arg === 'undefined'; +} +function isObject(arg) { + return typeof arg === 'object'; +} +function isPureObject(arg) { + return typeof arg === 'object' && arg !== null; +} +function isFunction(arg) { + return typeof arg === 'function'; +} +function isString(arg) { + return typeof arg === 'string'; } -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) { +function ensureIsArray(arg, key) { if (!key || !isObject(arg)) { - return isArray(arg) ? arg : [] + return isArray(arg) ? arg : []; } if (!isArray(arg[key])) { arg[key] = []; } - return arg + + return arg; } - -function ensuredPush (object, key, el) { +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; +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 - return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) +function inMetaInfoBranch(vm = this) { + return vm && !isUndefined(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) { +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 + return; } vm.$root._vueMeta.navGuards = true; - - var $router = vm.$root.$router; - var $meta = vm.$root.$meta(); - - $router.beforeEach(function (to, from, next) { + const $router = vm.$root.$router; + const $meta = vm.$root.$meta(); + $router.beforeEach((to, from, next) => { $meta.pause(); next(); }); + $router.afterEach(() => { + const { + metaInfo + } = $meta.resume(); - $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 hasGlobalWindowFn() { + try { + return !isUndefined(window); + } catch (e) { + return false; + } +} +const hasGlobalWindow = hasGlobalWindowFn(); -function createMixin (Vue, options) { +const _global = hasGlobalWindow ? window : global; + +const console = _global.console = _global.console || {}; +function warn(...args) { + /* istanbul ignore next */ + if (!console || !console.warn) { + return; + } + + console.warn(...args); +} + +let appId = 1; +function createMixin(Vue, options) { // for which Vue lifecycle hooks should the metaInfo be refreshed - var updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; // watch for client side component updates - // watch for client side component updates return { - beforeCreate: function beforeCreate () { - var this$1 = this; - + beforeCreate() { Object.defineProperty(this, '_hasMetaInfo', { configurable: true, - get: function get () { + + 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 + warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; } - return hasMetaInfo(this) - } - }); - // Add a marker to know if it uses metaInfo + 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 }; + this.$root._vueMeta = { + appId + }; appId++; - } - - // to speed up updates we keep track of branches which have a component with vue-meta info defined + } // 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; - 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 + } // 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 () { + ensuredPush(this.$options, 'created', () => { + this.$watch('$metaInfo', function () { triggerUpdate(this, 'watcher'); }); }); } - } - - // force an initial refresh on page load and prevent other lifecycleHooks + } // 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 () { + 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$1.$root.$el && this$1.$root.$el.hasAttribute && this$1.$root.$el.hasAttribute('data-server-rendered')) { - this$1.$root._vueMeta.appId = 'ssr'; + if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) { + this.$root._vueMeta.appId = options.ssrAppId; } - }); + }); // we use the mounted hook here as on page load - // we use the mounted hook here as on page load - ensuredPush(this.$options, 'mounted', function () { - if (!this$1.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'mounted', () => { + if (!this.$root._vueMeta.initialized) { // used in triggerUpdate to check if a change was triggered // during initialization - this$1.$root._vueMeta.initializing = true; + this.$root._vueMeta.initializing = true; // refresh meta in nextTick so all child components have loaded - // 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 + 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(function () { return triggerUpdate(this$1, 'initializing'); }); + 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 + 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 - // add the navigation guards if requested if (options.refreshOnceOnNavigation) { addNavGuards(this); } } - } + } // do not trigger refresh on the server side + - // 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); }); - }); + updateOnLifecycleHook.forEach(lifecycleHook => { + ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook)); + }); // re-render meta data when returning from a child component to parent - // re-render meta data when returning from a child component to parent - ensuredPush(this.$options, 'destroyed', function () { + ensuredPush(this.$options, 'destroyed', () => { // 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) { + const interval = setInterval(() => { + if (this.$el && this.$el.offsetParent !== null) { /* istanbul ignore next line */ - return + return; } clearInterval(interval); - if (!this$1.$parent) { + if (!this.$parent) { /* istanbul ignore next line */ - return + return; } - triggerUpdate(this$1, 'destroyed'); + triggerUpdate(this, 'destroyed'); }, 50); }); } } } - } + + }; } /** * These are constant variables used throughout the application. */ - // set some sane defaults -var defaultInfo = { +const defaultInfo = { title: undefined, titleChunk: '', titleTemplate: '%s', @@ -310,183 +312,112 @@ var defaultInfo = { script: [], noscript: [], __dangerouslyDisableSanitizers: [], - __dangerouslyDisableSanitizersByTagID: {} + __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. + }; - -// 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 +const 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` +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. -var ssrAttribute = 'data-vue-meta-server-rendered'; -// This is the property that tells vue-meta to overwrite (instead of append) +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. -var tagIDKeyName = 'vmid'; -// This is the key name for possible meta templates -var metaTemplateKeyName = 'template'; +const tagIDKeyName = 'vmid'; // This is the key name for possible meta templates -// This is the key name for the content-holding property -var contentKeyName = 'content'; +const metaTemplateKeyName = 'template'; // This is the key name for the content-holding property + +const contentKeyName = 'content'; // The id used for the ssr app + +const ssrAppId = 'ssr'; +const defaultOptions = { + keyName, + attribute, + ssrAttribute, + tagIDKeyName, + contentKeyName, + metaTemplateKeyName, + ssrAppId // List of metaInfo property keys which are configuration options (and dont generate html) -var defaultOptions = { - keyName: keyName, - attribute: attribute, - ssrAttribute: ssrAttribute, - tagIDKeyName: tagIDKeyName, - contentKeyName: contentKeyName, - metaTemplateKeyName: metaTemplateKeyName }; +const metaInfoOptionKeys = ['titleChunk', 'titleTemplate', 'changed', '__dangerouslyDisableSanitizers', '__dangerouslyDisableSanitizersByTagID']; // The metaInfo property keys which are used to disable escaping -// List of metaInfo property keys which are configuration options (and dont generate html) -var metaInfoOptionKeys = [ - 'titleChunk', - 'titleTemplate', - 'changed', - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' -]; +const disableOptionKeys = ['__dangerouslyDisableSanitizers', '__dangerouslyDisableSanitizersByTagID']; // List of metaInfo property keys which only generates attributes and no tags -// The metaInfo property keys which are used to disable escaping -var disableOptionKeys = [ - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' -]; +const metaInfoAttributeKeys = ['htmlAttrs', 'headAttrs', 'bodyAttrs']; // HTML elements which support the onload event -// 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) +const tagsSupportingOnload = ['link', 'style', 'script']; // 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']; +const tagsWithoutEndTag = ['base', 'meta', 'link']; // HTML elements which can have inner content (shortened to our needs) -// Attributes which are inserted as childNodes instead of HTMLAttribute -var tagAttributeAsInnerContent = ['innerHTML', 'cssText']; +const tagsWithInnerContent = ['noscript', 'script', 'style']; // Attributes which are inserted as childNodes instead of HTMLAttribute -// 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' -]; +const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json']; // Attributes which should be added with data- prefix -function setOptions (options) { +const commonDataAttributes = ['body', 'pbody']; // 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']; + +function setOptions(options) { // combine options options = isObject(options) ? options : {}; - for (var key in defaultOptions) { + for (const key in defaultOptions) { if (!options[key]) { options[key] = defaultOptions[key]; } } - return options + return options; } +function getOptions(options) { + const optionsCopy = {}; -function getOptions (options) { - var optionsCopy = {}; - for (var key in options) { + for (const key in options) { optionsCopy[key] = options[key]; } - return optionsCopy + + return optionsCopy; } -function pause (refresh) { - if ( refresh === void 0 ) refresh = true; - +function pause(refresh = true) { this.$root._vueMeta.paused = true; - - return function () { return resume(refresh); } + return () => resume(refresh); } - -function resume (refresh) { - if ( refresh === void 0 ) refresh = true; - +function resume(refresh = true) { this.$root._vueMeta.paused = false; if (refresh) { - return this.$root.$meta().refresh() + return this.$root.$meta().refresh(); } } -function applyTemplate (ref, headObject, template, chunk) { - var component = ref.component; - var metaTemplateKeyName = ref.metaTemplateKeyName; - var contentKeyName = ref.contentKeyName; - +function applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName +}, headObject, template, chunk) { if (isUndefined(template)) { template = headObject[metaTemplateKeyName]; delete headObject[metaTemplateKeyName]; - } + } // return early if no template defined + - // return early if no template defined if (!template) { - return false + return false; } if (isUndefined(chunk)) { chunk = headObject[contentKeyName]; } - headObject[contentKeyName] = isFunction(template) - ? template.call(component, chunk) - : template.replace(/%s/g, chunk); - - return true + headObject[contentKeyName] = isFunction(template) ? template.call(component, chunk) : template.replace(/%s/g, chunk); + return true; } /* @@ -497,179 +428,185 @@ function applyTemplate (ref, headObject, template, chunk) { * 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; - +function findIndex(array, predicate) { 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 + for (let idx = 0; idx < array.length; idx++) { + if (predicate.call(arguments[2], array[idx], idx, array)) { + return idx; } } - return -1 - } - return array.findIndex(predicate, arguments[2]) -} -function toArray (arg) { + return -1; + } + + return array.findIndex(predicate, arguments[2]); +} +function toArray(arg) { if ( !Array.from) { - return Array.prototype.slice.call(arg) + return Array.prototype.slice.call(arg); } - return Array.from(arg) -} -function includes (array, value) { + return Array.from(arg); +} +function includes(array, value) { if ( !Array.prototype.includes) { - for (var idx in array) { + for (const idx in array) { if (array[idx] === value) { - return true + return true; } } - return false + return false; } - return array.includes(value) + + return array.includes(value); } -var serverSequences = [ - [/&/g, '&'], - [/</g, '<'], - [/>/g, '>'], - [/"/g, '"'], - [/'/g, '''] -]; +const serverSequences = [[/&/g, '&'], [/</g, '<'], [/>/g, '>'], [/"/g, '"'], [/'/g, ''']]; +const clientSequences = [[/&/g, '\u0026'], [/</g, '\u003C'], [/>/g, '\u003E'], [/"/g, '\u0022'], [/'/g, '\u0027']]; // sanitizes potentially dangerous characters -var clientSequences = [ - [/&/g, '\u0026'], - [/</g, '\u003C'], - [/>/g, '\u003E'], - [/"/g, '\u0022'], - [/'/g, '\u0027'] -]; +function escape(info, options, escapeOptions) { + const { + tagIDKeyName + } = options; + const { + doEscape = v => v, + escapeKeys + } = escapeOptions; + const escaped = {}; -// 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 (const key in info) { + const value = info[key]; // no need to escape configuration options - for (var key in info) { - var value = info[key]; - - // no need to escape configuration options if (includes(metaInfoOptionKeys, key)) { escaped[key] = value; - continue + continue; } - var disableKey = disableOptionKeys[0]; + 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 + continue; } - var tagId = info[tagIDKeyName]; + const tagId = info[tagIDKeyName]; + if (tagId) { - disableKey = disableOptionKeys[1]; + disableKey = disableOptionKeys[1]; // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped - // 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 + 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) + escaped[key] = value.map(v => { + if (isPureObject(v)) { + return escape(v, options, { ...escapeOptions, + escapeKeys: true + }); + } + + return doEscape(v); + }); + } else if (isPureObject(value)) { + escaped[key] = escape(value, options, { ...escapeOptions, + escapeKeys: true }); - } else if (isObject(value)) { - escaped[key] = escape(value, options, escapeOptions); } else { escaped[key] = value; } + + if (escapeKeys) { + const escapedKey = doEscape(key); + + if (key !== escapedKey) { + escaped[escapedKey] = escaped[key]; + delete escaped[key]; + } + } } - return escaped + return escaped; } -function arrayMerge (ref, target, source) { - var component = ref.component; - var tagIDKeyName = ref.tagIDKeyName; - var metaTemplateKeyName = ref.metaTemplateKeyName; - var contentKeyName = ref.contentKeyName; - +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 - var destination = []; - - target.forEach(function (targetItem, targetIndex) { + const destination = []; + target.forEach((targetItem, targetIndex) => { // no tagID so no need to check for duplicity if (!targetItem[tagIDKeyName]) { destination.push(targetItem); - return + return; } - var sourceIndex = findIndex(source, function (item) { return item[tagIDKeyName] === targetItem[tagIDKeyName]; }); - var sourceItem = source[sourceIndex]; + const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName]); + const sourceItem = source[sourceIndex]; // source doesnt contain any duplicate vmid's, we can keep targetItem - // 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 + 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 + + + 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 - + 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 - } + return; + } // now we only need to check if the target has a template to combine it with the source + + + const targetTemplate = targetItem[metaTemplateKeyName]; - // 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 + return; } - var sourceTemplate = sourceItem[metaTemplateKeyName]; + const sourceTemplate = sourceItem[metaTemplateKeyName]; if (!sourceTemplate) { // use parent template and child content - applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, targetTemplate); + applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, sourceItem, targetTemplate); } else if (!sourceItem[contentKeyName]) { // use child template and parent content - applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, sourceItem, undefined, targetItem[contentKeyName]); } }); - - return destination.concat(source) + return destination.concat(source); } - -function merge (target, source, options) { - if ( options === void 0 ) options = {}; - +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) @@ -677,25 +614,24 @@ function merge (target, source, options) { delete source.title; } - metaInfoAttributeKeys.forEach(function (attrKey) { + metaInfoAttributeKeys.forEach(attrKey => { if (!source[attrKey]) { - return + return; } - for (var key in source[attrKey]) { + for (const key in source[attrKey]) { if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { - if (booleanHtmlAttributes.includes(key)) { - // eslint-disable-next-line no-console - console.warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); + if (includes(booleanHtmlAttributes, key)) { + warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); } + delete source[attrKey][key]; } } }); - return deepmerge(target, source, { - arrayMerge: function (t, s) { return arrayMerge(options, t, s); } - }) + arrayMerge: (t, s) => arrayMerge(options, t, s) + }); } /** @@ -712,45 +648,46 @@ function merge (target, source, options) { * @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; +function getComponentOption(options = {}, component, result = {}) { + const { + keyName, + metaTemplateKeyName, + tagIDKeyName + } = options; + const { + $options, + $children + } = component; if (component._inactive) { - return result - } + return result; + } // only collect option data if it exists + - // only collect option data if it exists if ($options[keyName]) { - var data = $options[keyName]; + let data = $options[keyName]; // if option is a function, replace it with it's result - // 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 + - // ignore data if its not an object, then we keep our previous result if (!isObject(data)) { - return result - } + return result; + } // merge with existing options + - // merge with existing options result = merge(result, data, options); - } + } // collect & aggregate child options if deep = true + - // collect & aggregate child options if deep = true if ($children.length) { - $children.forEach(function (childComponent) { + $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 + return; } result = getComponentOption(options, childComponent, result); @@ -759,20 +696,17 @@ function getComponentOption (options, component, result) { if (metaTemplateKeyName && result.meta) { // apply templates if needed - result.meta.forEach(function (metaObject) { return applyTemplate(options, metaObject); }); + result.meta.forEach(metaObject => applyTemplate(options, metaObject)); // remove meta items with duplicate vmid's - // 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]; }) - ) + 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 + return result; } /** @@ -782,56 +716,180 @@ function getComponentOption (options, component, result) { * @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 = []; +function getMetaInfo(options = {}, component, escapeSequences = []) { // collect & aggregate all metaInfo $options - var info = getComponentOption(options, component, defaultInfo); - - // Remove all "template" tags from meta - + 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 + - // 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 + 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] : []; } - 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); } + const escapeOptions = { + doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value) }; - - disableOptionKeys.forEach(function (disableKey, index) { + disableOptionKeys.forEach((disableKey, index) => { if (index === 0) { ensureIsArray(info, disableKey); } else if (index === 1) { - for (var key in info[disableKey]) { + for (const key in info[disableKey]) { ensureIsArray(info[disableKey], key); } } escapeOptions[disableKey] = info[disableKey]; - }); + }); // begin sanitization - // begin sanitization info = escape(info, options, escapeOptions); + return info; +} - return info +function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag]; +} +function getElementsKey({ + body, + pbody +}) { + return body ? 'body' : pbody ? 'pbody' : 'head'; +} +function queryElements(parentNode, { + appId, + attribute, + type, + tagIDKeyName +}, attributes = {}) { + const queries = [`${type}[${attribute}="${appId}"]`, `${type}[data-${tagIDKeyName}]`].map(query => { + for (const key in attributes) { + const val = attributes[key]; + const attributeValue = val && val !== true ? `="${val}"` : ''; + query += `[data-${key}${attributeValue}]`; + } + + return query; + }); + return toArray(parentNode.querySelectorAll(queries.join(', '))); +} + +const callbacks = []; +function isDOMComplete(d = document) { + return d.readyState === 'complete'; +} +function addCallback(query, callback) { + if (arguments.length === 1) { + callback = query; + query = ''; + } + + callbacks.push([query, callback]); +} +function addCallbacks({ + tagIDKeyName +}, type, tags, autoAddListeners) { + let hasAsyncCallback = false; + + for (const tag of tags) { + if (!tag[tagIDKeyName] || !tag.callback) { + continue; + } + + hasAsyncCallback = true; + addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback); + } + + if (!autoAddListeners || !hasAsyncCallback) { + return hasAsyncCallback; + } + + return addListeners(); +} +function addListeners() { + if (isDOMComplete()) { + applyCallbacks(); + return; + } // Instead of using a MutationObserver, we just apply + + /* istanbul ignore next */ + + + document.onreadystatechange = () => { + applyCallbacks(); + }; +} +function applyCallbacks(matchElement) { + for (const [query, callback] of callbacks) { + const selector = `${query}[onload="this.__vm_l=1"]`; + let elements = []; + + if (!matchElement) { + elements = toArray(document.querySelectorAll(selector)); + } + + if (matchElement && matchElement.matches(selector)) { + elements = [matchElement]; + } + + for (const element of elements) { + /* __vm_cb: whether the load callback has been called + * __vm_l: set by onload attribute, whether the element was loaded + * __vm_ev: whether the event listener was added or not + */ + if (element.__vm_cb) { + continue; + } + + const onload = () => { + /* Mark that the callback for this element has already been called, + * this prevents the callback to run twice in some (rare) conditions + */ + element.__vm_cb = true; + /* onload needs to be removed because we only need the + * attribute after ssr and if we dont remove it the node + * will fail isEqualNode on the client + */ + + element.removeAttribute('onload'); + callback(element); + }; + /* IE9 doesnt seem to load scripts synchronously, + * causing a script sometimes/often already to be loaded + * when we add the event listener below (thus adding an onload event + * listener has no use because it will never be triggered). + * Therefore we add the onload attribute during ssr, and + * check here if it was already loaded or not + */ + + + if (element.__vm_l) { + onload(); + continue; + } + + if (!element.__vm_ev) { + element.__vm_ev = true; + element.addEventListener('load', onload); + } + } + } } /** @@ -840,43 +898,38 @@ function getMetaInfo (options, component, escapeSequences) { * @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); +function updateAttribute({ + attribute +} = {}, attrs, tag) { + const vueMetaAttrString = tag.getAttribute(attribute); + const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + const toRemove = toArray(vueMetaAttrs); + const keepIndexes = []; - var keepIndexes = []; - for (var attr in attrs) { + for (const attr in attrs) { if (attrs.hasOwnProperty(attr)) { - var value = includes(booleanHtmlAttributes, attr) - ? '' - : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[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 + - // 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); + 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(',')); + tag.setAttribute(attribute, vueMetaAttrs.sort().join(',')); } } @@ -885,9 +938,9 @@ function updateAttribute (ref, attrs, tag) { * * @param {String} title - the new title of the document */ -function updateTitle (title) { - if (title === undefined) { - return +function updateTitle(title) { + if (!title && title !== '') { + return; } document.title = title; @@ -901,98 +954,143 @@ function updateTitle (title) { * @param {(Array<Object>|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 = []; +function updateTag(appId, options = {}, type, tags, head, body) { + const { + attribute, + tagIDKeyName + } = options; + const dataAttributes = [tagIDKeyName, ...commonDataAttributes]; + const newElements = []; + const queryOptions = { + appId, + attribute, + type, + tagIDKeyName + }; + const currentElements = { + head: queryElements(head, queryOptions), + pbody: queryElements(body, queryOptions, { + pbody: true + }), + body: queryElements(body, queryOptions, { + body: true + }) + }; 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); + const found = []; + tags = tags.filter(x => { + const k = JSON.stringify(x); + const res = !includes(found, k); found.push(k); - return res + 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 isBooleanAttribute = includes(booleanHtmlAttributes, attr); - if (isBooleanAttribute && !tag[attr]) { - continue - } - - var value = isBooleanAttribute ? '' : tag[attr]; - newElement.setAttribute(_attr, value); - } - } + for (const tag of tags) { + if (tag.skip) { + continue; } - // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. - var indexToDelete; - var hasEqualElement = oldTags.some(function (existingTag, index) { + const newElement = document.createElement(type); + newElement.setAttribute(attribute, appId); + + for (const attr in tag) { + /* istanbul ignore next */ + if (!tag.hasOwnProperty(attr)) { + continue; + } + + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + continue; + } + + if (attr === 'json') { + newElement.innerHTML = JSON.stringify(tag.json); + continue; + } + + if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + + continue; + } + + if (attr === 'callback') { + newElement.onload = () => tag[attr](newElement); + + continue; + } + + const _attr = includes(dataAttributes, attr) ? `data-${attr}` : attr; + + const isBooleanAttribute = includes(booleanHtmlAttributes, attr); + + if (isBooleanAttribute && !tag[attr]) { + continue; + } + + const value = isBooleanAttribute ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + + const oldElements = currentElements[getElementsKey(tag)]; // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + + let indexToDelete; + const hasEqualElement = oldElements.some((existingTag, index) => { indexToDelete = index; - return newElement.isEqualNode(existingTag) + return newElement.isEqualNode(existingTag); }); if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { - oldTags.splice(indexToDelete, 1); + oldElements.splice(indexToDelete, 1); } else { - newTags.push(newElement); + newElements.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] + let oldElements = []; + + for (const current of Object.values(currentElements)) { + oldElements = [...oldElements, ...current]; + } // remove old elements + + + for (const element of oldElements) { + element.parentNode.removeChild(element); + } // insert new elements + + + for (const element of newElements) { + if (element.hasAttribute('data-body')) { + body.appendChild(element); + continue; + } + + if (element.hasAttribute('data-pbody')) { + body.insertBefore(element, body.firstChild); + continue; + } + + head.appendChild(element); + } + + return { + oldTags: oldElements, + newTags: newElements + }; } /** @@ -1000,60 +1098,66 @@ function getTag (tags, tag) { * * @param {Object} newInfo - the meta info to update to */ -function updateClientMetaInfo (appId, options, newInfo) { - if ( options === void 0 ) options = {}; - var ssrAttribute = options.ssrAttribute; +function updateClientMetaInfo(appId, options = {}, newInfo) { + const { + ssrAttribute, + ssrAppId + } = options; // only cache tags for current update - // only cache tags for current update - var tags = {}; + const tags = {}; + const htmlTag = getTag(tags, 'html'); // if this is a server render, then dont update - var htmlTag = getTag(tags, 'html'); - - // if this is a server render, then dont update - if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) { // remove the server render attribute so we can update on (next) changes - htmlTag.removeAttribute(ssrAttribute); - return false - } + htmlTag.removeAttribute(ssrAttribute); // add load callbacks if the - // initialize tracked changes - var addedTags = {}; - var removedTags = {}; + let addLoadListeners = false; - for (var type in newInfo) { + for (const type of tagsSupportingOnload) { + if (newInfo[type] && addCallbacks(options, type, newInfo[type])) { + addLoadListeners = true; + } + } + + if (addLoadListeners) { + addListeners(); + } + + return false; + } // initialize tracked changes + + + const addedTags = {}; + const removedTags = {}; + + for (const type in newInfo) { // ignore these if (includes(metaInfoOptionKeys, type)) { - continue + continue; } if (type === 'title') { // update the title updateTitle(newInfo.title); - continue + continue; } if (includes(metaInfoAttributeKeys, type)) { - var tagName = type.substr(0, 4); + const tagName = type.substr(0, 4); updateAttribute(options, newInfo[type], getTag(tags, tagName)); - continue - } + continue; + } // tags should always be an array, ignore if it isnt + - // tags should always be an array, ignore if it isnt if (!isArray(newInfo[type])) { - continue + continue; } - var ref = updateTag( - appId, - options, - type, - newInfo[type], - getTag(tags, 'head'), - getTag(tags, 'body') - ); - var oldTags = ref.oldTags; - var newTags = ref.newTags; + const { + oldTags, + newTags + } = updateTag(appId, options, type, newInfo[type], getTag(tags, 'head'), getTag(tags, 'body')); if (newTags.length) { addedTags[type] = newTags; @@ -1061,12 +1165,13 @@ function updateClientMetaInfo (appId, options, newInfo) { } } - return { addedTags: addedTags, removedTags: removedTags } + return { + addedTags, + removedTags + }; } -function _refresh (options) { - if ( options === void 0 ) options = {}; - +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 @@ -1077,18 +1182,21 @@ function _refresh (options) { * * @return {Object} - new meta info */ - return function refresh () { - var metaInfo = getMetaInfo(options, this.$root, clientSequences); + 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 - 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 } - } + return { + vm: this, + metaInfo, + tags + }; + }; } /** @@ -1098,31 +1206,36 @@ function _refresh (options) { * @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; +function attributeGenerator({ + attribute, + ssrAttribute +} = {}, type, data) { return { - text: function text () { - var attributeStr = ''; - var watchedAttrs = []; + text(addSrrAttribute) { + let attributeStr = ''; + const watchedAttrs = []; - for (var attr in data) { + for (const 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 += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) ? attr : `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"`; attributeStr += ' '; } } - attributeStr += attribute + "=\"" + ((watchedAttrs.sort()).join(',')) + "\""; - return attributeStr + if (attributeStr) { + attributeStr += `${attribute}="${watchedAttrs.sort().join(',')}"`; + } + + if (type === 'htmlAttrs' && addSrrAttribute) { + return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}`; + } + + return attributeStr; } - } + + }; } /** @@ -1132,15 +1245,19 @@ function attributeGenerator (ref, type, data) { * @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; - +function titleGenerator({ + attribute +} = {}, type, data) { return { - text: function text () { - return ("<" + type + ">" + data + "</" + type + ">") + text() { + if (!data) { + return ''; + } + + return `<${type}>${data}</${type}>`; } - } + + }; } /** @@ -1150,72 +1267,82 @@ function titleGenerator (appId, ref, type, data) { * @param {(Array<Object>|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; +function tagGenerator({ + ssrAppId, + attribute, + tagIDKeyName +} = {}, type, tags) { + const dataAttributes = [tagIDKeyName, 'callback', ...commonDataAttributes]; return { - text: function text (ref) { - if ( ref === void 0 ) ref = {}; - var body = ref.body; if ( body === void 0 ) body = false; - + text({ + body = false, + pbody = false + } = {}) { // build a string containing all tags of this type - return tags.reduce(function (tagsStr, tag) { - var tagKeys = Object.keys(tag); + return tags.reduce((tagsStr, tag) => { + if (tag.skip) { + return tagsStr; + } + + const tagKeys = Object.keys(tag); if (tagKeys.length === 0) { - return tagsStr // Bail on empty tag object + return tagsStr; // Bail on empty tag object } - if (Boolean(tag.body) !== body) { - return tagsStr + if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) { + return tagsStr; } - // build a string containing all attributes of this tag - var attrs = tagKeys.reduce(function (attrsStr, attr) { + let attrs = tag.once ? '' : ` ${attribute}="${ssrAppId}"`; // build a string containing all attributes of this tag + + for (const attr in tag) { // these attributes are treated as children on the tag if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') { - return attrsStr - } + continue; + } // these form the attribute list for this tag - // these form the attribute list for this tag - var prefix = ''; - if ([tagIDKeyName, 'body'].includes(attr)) { + + let prefix = ''; + + if (dataAttributes.includes(attr)) { prefix = 'data-'; } - var isBooleanAttr = booleanHtmlAttributes.includes(attr); - if (isBooleanAttr && !tag[attr]) { - return attrsStr + if (attr === 'callback') { + attrs += ` onload="this.__vm_l=1"`; + continue; } - return isBooleanAttr - ? (attrsStr + " " + prefix + attr) - : (attrsStr + " " + prefix + attr + "=\"" + (tag[attr]) + "\"") - }, ''); + const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr); - // grab child content from one of these attributes, if possible - var content = tag.innerHTML || tag.cssText || ''; + if (isBooleanAttr && !tag[attr]) { + continue; + } - // generate tag exactly without any other redundant attribute - var observeTag = tag.once - ? '' - : (attribute + "=\"" + appId + "\""); + attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`); + } + let json = ''; + + if (tag.json) { + json = JSON.stringify(tag.json); + } // grab child content from one of these attributes, if possible + + + const content = tag.innerHTML || tag.cssText || json; // generate tag exactly without any other redundant attribute // these tags have no end tag - var hasEndTag = !tagsWithoutEndTag.includes(type); - // these tag types will have content inserted - var hasContent = hasEndTag && tagsWithInnerContent.includes(type); + const hasEndTag = !tagsWithoutEndTag.includes(type); // these tag types will have content inserted - // the final string for this specific tag - return !hasContent - ? (tagsStr + "<" + type + " " + observeTag + attrs + (hasEndTag ? '/' : '') + ">") - : (tagsStr + "<" + type + " " + observeTag + attrs + ">" + content + "</" + type + ">") - }, '') + const hasContent = hasEndTag && tagsWithInnerContent.includes(type); // the final string for this specific tag + + return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` + (hasContent ? `${content}</${type}>` : ''); + }, ''); } - } + + }; } /** @@ -1226,21 +1353,19 @@ function tagGenerator (appId, ref, type, tags) { * @return {Object} - the new injector */ -function generateServerInjector (appId, options, type, data) { +function generateServerInjector(options, type, data) { if (type === 'title') { - return titleGenerator(appId, options, type, data) + return titleGenerator(options, type, data); } if (metaInfoAttributeKeys.includes(type)) { - return attributeGenerator(options, type, data) + return attributeGenerator(options, type, data); } - return tagGenerator(appId, options, type, data) + return tagGenerator(options, type, data); } -function _inject (options) { - if ( options === void 0 ) options = {}; - +function _inject(options = {}) { /** * Converts the state of the meta info object such that each item * can be compiled to a tag string on the server @@ -1248,66 +1373,62 @@ function _inject (options) { * @this {Object} - Vue instance - ideally the root component * @return {Object} - server meta info with `toString` methods */ - return function inject () { + return function inject() { // get meta info with sensible defaults - var metaInfo = getMetaInfo(options, this.$root, serverSequences); + const metaInfo = getMetaInfo(options, this.$root, serverSequences); // generate server injectors - // generate server injectors - for (var key in metaInfo) { + for (const key in metaInfo) { if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) { - metaInfo[key] = generateServerInjector('ssr', options, key, metaInfo[key]); + metaInfo[key] = generateServerInjector(options, key, metaInfo[key]); } } - return metaInfo - } + return metaInfo; + }; } -function _$meta (options) { - if ( options === void 0 ) options = {}; - - var _refresh$1 = _refresh(options); - var _inject$1 = _inject(options); +function _$meta(options = {}) { + const _refresh$1 = _refresh(options); + const _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 function $meta() { return { - getOptions: function () { return getOptions(options); }, + getOptions: () => 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 = {}; +function install(Vue, options = {}) { if (Vue.__vuemeta_installed) { - return + 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 + version, + install, + hasMetaInfo }; module.exports = index; diff --git a/dist/vue-meta.esm.browser.js b/dist/vue-meta.esm.browser.js index c75dcd5..09d4d9a 100644 --- a/dist/vue-meta.esm.browser.js +++ b/dist/vue-meta.esm.browser.js @@ -1,5 +1,5 @@ /** - * vue-meta v2.0.5 + * vue-meta v2.1.0 * (c) 2019 * - Declan de Wet * - Sébastien Chopin (@Atinux) @@ -9,7 +9,7 @@ import deepmerge from 'deepmerge'; -var version = "2.0.5"; +var version = "2.1.0"; // store an id to keep track of DOM updates let batchId = null; @@ -62,6 +62,10 @@ function isObject (arg) { return typeof arg === 'object' } +function isPureObject (arg) { + return typeof arg === 'object' && arg !== null +} + function isFunction (arg) { return typeof arg === 'function' } @@ -122,6 +126,31 @@ function addNavGuards (vm) { }); } +function hasGlobalWindowFn () { + try { + return !isUndefined(window) + } catch (e) { + return false + } +} + +const hasGlobalWindow = hasGlobalWindowFn(); + +const _global = hasGlobalWindow ? window : global; + +const console = (_global.console = _global.console || {}); + +function warn (...args) { + /* istanbul ignore next */ + if (!console || !console.warn) { + return + } + + console.warn(...args); +} + +const showWarningNotSupported = () => warn('This vue app/component has no vue-meta configuration'); + let appId = 1; function createMixin (Vue, options) { @@ -136,7 +165,7 @@ function createMixin (Vue, options) { 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 + warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; } return hasMetaInfo(this) @@ -198,7 +227,7 @@ function createMixin (Vue, options) { // 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 && this.$root.$el.hasAttribute('data-server-rendered')) { - this.$root._vueMeta.appId = 'ssr'; + this.$root._vueMeta.appId = options.ssrAppId; } }); @@ -320,13 +349,17 @@ const metaTemplateKeyName = 'template'; // This is the key name for the content-holding property const contentKeyName = 'content'; +// The id used for the ssr app +const ssrAppId = 'ssr'; + const defaultOptions = { keyName, attribute, ssrAttribute, tagIDKeyName, contentKeyName, - metaTemplateKeyName + metaTemplateKeyName, + ssrAppId }; // List of metaInfo property keys which are configuration options (and dont generate html) @@ -351,6 +384,12 @@ const metaInfoAttributeKeys = [ 'bodyAttrs' ]; +// HTML elements which support the onload event +const tagsSupportingOnload = ['link', 'style', 'script']; + +// Attributes which should be added with data- prefix +const commonDataAttributes = ['body', 'pbody']; + // from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202 const booleanHtmlAttributes = [ 'allowfullscreen', @@ -397,9 +436,6 @@ const booleanHtmlAttributes = [ '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 : {}; @@ -489,7 +525,7 @@ const clientSequences = [ // sanitizes potentially dangerous characters function escape (info, options, escapeOptions) { const { tagIDKeyName } = options; - const { doEscape = v => v } = escapeOptions; + const { doEscape = v => v, escapeKeys } = escapeOptions; const escaped = {}; for (const key in info) { @@ -523,15 +559,25 @@ function escape (info, options, escapeOptions) { escaped[key] = doEscape(value); } else if (isArray(value)) { escaped[key] = value.map((v) => { - return isObject(v) - ? escape(v, options, escapeOptions) - : doEscape(v) + if (isPureObject(v)) { + return escape(v, options, { ...escapeOptions, escapeKeys: true }) + } + + return doEscape(v) }); - } else if (isObject(value)) { - escaped[key] = escape(value, options, escapeOptions); + } else if (isPureObject(value)) { + escaped[key] = escape(value, options, { ...escapeOptions, escapeKeys: true }); } else { escaped[key] = value; } + + if (escapeKeys) { + const escapedKey = doEscape(key); + if (key !== escapedKey) { + escaped[escapedKey] = escaped[key]; + delete escaped[key]; + } + } } return escaped @@ -614,9 +660,8 @@ function merge (target, source, options = {}) { for (const key in source[attrKey]) { if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { - if (booleanHtmlAttributes.includes(key)) { - // eslint-disable-next-line no-console - console.warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); + if (includes(booleanHtmlAttributes, key)) { + warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); } delete source[attrKey][key]; } @@ -750,6 +795,141 @@ function getMetaInfo (options = {}, component, escapeSequences = []) { return info } +function getTag (tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag] +} + +function getElementsKey ({ body, pbody }) { + return body + ? 'body' + : (pbody ? 'pbody' : 'head') +} + +function queryElements (parentNode, { appId, attribute, type, tagIDKeyName }, attributes = {}) { + const queries = [ + `${type}[${attribute}="${appId}"]`, + `${type}[data-${tagIDKeyName}]` + ].map((query) => { + for (const key in attributes) { + const val = attributes[key]; + const attributeValue = val && val !== true ? `="${val}"` : ''; + query += `[data-${key}${attributeValue}]`; + } + return query + }); + + return toArray(parentNode.querySelectorAll(queries.join(', '))) +} + +const callbacks = []; + +function isDOMComplete (d = document) { + return d.readyState === 'complete' +} + +function addCallback (query, callback) { + if (arguments.length === 1) { + callback = query; + query = ''; + } + + callbacks.push([ query, callback ]); +} + +function addCallbacks ({ tagIDKeyName }, type, tags, autoAddListeners) { + let hasAsyncCallback = false; + + for (const tag of tags) { + if (!tag[tagIDKeyName] || !tag.callback) { + continue + } + + hasAsyncCallback = true; + addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback); + } + + if (!autoAddListeners || !hasAsyncCallback) { + return hasAsyncCallback + } + + return addListeners() +} + +function addListeners () { + if (isDOMComplete()) { + applyCallbacks(); + return + } + + // Instead of using a MutationObserver, we just apply + /* istanbul ignore next */ + document.onreadystatechange = () => { + applyCallbacks(); + }; +} + +function applyCallbacks (matchElement) { + for (const [query, callback] of callbacks) { + const selector = `${query}[onload="this.__vm_l=1"]`; + + let elements = []; + if (!matchElement) { + elements = toArray(document.querySelectorAll(selector)); + } + + if (matchElement && matchElement.matches(selector)) { + elements = [matchElement]; + } + + for (const element of elements) { + /* __vm_cb: whether the load callback has been called + * __vm_l: set by onload attribute, whether the element was loaded + * __vm_ev: whether the event listener was added or not + */ + if (element.__vm_cb) { + continue + } + + const onload = () => { + /* Mark that the callback for this element has already been called, + * this prevents the callback to run twice in some (rare) conditions + */ + element.__vm_cb = true; + + /* onload needs to be removed because we only need the + * attribute after ssr and if we dont remove it the node + * will fail isEqualNode on the client + */ + element.removeAttribute('onload'); + + callback(element); + }; + + /* IE9 doesnt seem to load scripts synchronously, + * causing a script sometimes/often already to be loaded + * when we add the event listener below (thus adding an onload event + * listener has no use because it will never be triggered). + * Therefore we add the onload attribute during ssr, and + * check here if it was already loaded or not + */ + if (element.__vm_l) { + onload(); + continue + } + + if (!element.__vm_ev) { + element.__vm_ev = true; + + element.addEventListener('load', onload); + } + } + } +} + /** * Updates the document's html tag attributes * @@ -799,7 +979,7 @@ function updateAttribute ({ attribute } = {}, attrs, tag) { * @param {String} title - the new title of the document */ function updateTitle (title) { - if (title === undefined) { + if (!title && title !== '') { return } @@ -814,11 +994,18 @@ function updateTitle (title) { * @param {(Array<Object>|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 = []; +function updateTag (appId, options = {}, type, tags, head, body) { + const { attribute, tagIDKeyName } = options; + + const dataAttributes = [tagIDKeyName, ...commonDataAttributes]; + const newElements = []; + + const queryOptions = { appId, attribute, type, tagIDKeyName }; + const currentElements = { + head: queryElements(head, queryOptions), + pbody: queryElements(body, queryOptions, { pbody: true }), + body: queryElements(body, queryOptions, { body: true }) + }; if (tags.length > 1) { // remove duplicates that could have been found by merging tags @@ -834,74 +1021,107 @@ function updateTag (appId, { attribute, tagIDKeyName } = {}, type, tags, headTag } if (tags.length) { - tags.forEach((tag) => { - const newElement = document.createElement(type); + for (const tag of tags) { + if (tag.skip) { + continue + } + 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 isBooleanAttribute = includes(booleanHtmlAttributes, attr); - if (isBooleanAttribute && !tag[attr]) { - continue - } - - const value = isBooleanAttribute ? '' : tag[attr]; - newElement.setAttribute(_attr, value); - } + /* istanbul ignore next */ + if (!tag.hasOwnProperty(attr)) { + continue } + + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + continue + } + + if (attr === 'json') { + newElement.innerHTML = JSON.stringify(tag.json); + continue + } + + if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + continue + } + + if (attr === 'callback') { + newElement.onload = () => tag[attr](newElement); + continue + } + + const _attr = includes(dataAttributes, attr) + ? `data-${attr}` + : attr; + + const isBooleanAttribute = includes(booleanHtmlAttributes, attr); + if (isBooleanAttribute && !tag[attr]) { + continue + } + + const value = isBooleanAttribute ? '' : tag[attr]; + newElement.setAttribute(_attr, value); } + const oldElements = currentElements[getElementsKey(tag)]; + // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. let indexToDelete; - const hasEqualElement = oldTags.some((existingTag, index) => { + const hasEqualElement = oldElements.some((existingTag, index) => { indexToDelete = index; return newElement.isEqualNode(existingTag) }); if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { - oldTags.splice(indexToDelete, 1); + oldElements.splice(indexToDelete, 1); } else { - newTags.push(newElement); + newElements.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] + let oldElements = []; + for (const current of Object.values(currentElements)) { + oldElements = [ + ...oldElements, + ...current + ]; + } + + // remove old elements + for (const element of oldElements) { + element.parentNode.removeChild(element); + } + + // insert new elements + for (const element of newElements) { + if (element.hasAttribute('data-body')) { + body.appendChild(element); + continue + } + + if (element.hasAttribute('data-pbody')) { + body.insertBefore(element, body.firstChild); + continue + } + + head.appendChild(element); + } + + return { + oldTags: oldElements, + newTags: newElements + } } /** @@ -910,7 +1130,7 @@ function getTag (tags, tag) { * @param {Object} newInfo - the meta info to update to */ function updateClientMetaInfo (appId, options = {}, newInfo) { - const { ssrAttribute } = options; + const { ssrAttribute, ssrAppId } = options; // only cache tags for current update const tags = {}; @@ -918,9 +1138,22 @@ function updateClientMetaInfo (appId, options = {}, newInfo) { const htmlTag = getTag(tags, 'html'); // if this is a server render, then dont update - if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) { // remove the server render attribute so we can update on (next) changes htmlTag.removeAttribute(ssrAttribute); + + // add load callbacks if the + let addLoadListeners = false; + for (const type of tagsSupportingOnload) { + if (newInfo[type] && addCallbacks(options, type, newInfo[type])) { + addLoadListeners = true; + } + } + + if (addLoadListeners) { + addListeners(); + } + return false } diff --git a/dist/vue-meta.esm.browser.min.js b/dist/vue-meta.esm.browser.min.js index 01b66f7..0b0749e 100644 --- a/dist/vue-meta.esm.browser.min.js +++ b/dist/vue-meta.esm.browser.min.js @@ -1 +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:void 0,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 w=[[/&/g,"&"],[/</g,"<"],[/>/g,">"],[/"/g,'"'],[/'/g,"'"]];function I(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]&&($.includes(t)&&console.warn("VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details"),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=I(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,{attribute:t,tagIDKeyName:n}={},i,o,a,r){const s=T(a.querySelectorAll(`${i}[${t}="${e}"], ${i}[data-${n}]`)),u=T(r.querySelectorAll(`${i}[${t}="${e}"][data-body="true"], ${i}[data-${n}][data-body="true"]`)),c=[n,"body"],l=[];if(o.length>1){const e=[];o=o.filter(t=>{const n=JSON.stringify(t),i=!N(e,n);return e.push(n),i})}o.length&&o.forEach(n=>{const o=document.createElement(i);o.setAttribute(t,e);const a=!0!==n.body?s:u;for(const e in n)if(n.hasOwnProperty(e))if("innerHTML"===e)o.innerHTML=n.innerHTML;else if("cssText"===e)o.styleSheet?o.styleSheet.cssText=n.cssText:o.appendChild(document.createTextNode(n.cssText));else{const t=N(c,e)?`data-${e}`:e,i=N($,e);if(i&&!n[e])continue;const a=i?"":n[e];o.setAttribute(t,a)}let r;a.some((e,t)=>(r=t,o.isEqualNode(e)))&&(r||0===r)?a.splice(r,1):l.push(o)});const d=s.concat(u);return d.forEach(e=>e.parentNode.removeChild(e)),l.forEach(e=>{"true"===e.getAttribute("data-body")?r.appendChild(e):a.appendChild(e)}),{oldTags:d,newTags:l}}function O(e,t){return e[t]||(e[t]=document.getElementsByTagName(t)[0]),e[t]}function k(e={}){return function(){const t=z(e,this.$root,w),n=function(e,t={},n){const{ssrAttribute:o}=t,a={},r=O(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){void 0!==(c=n.title)&&(document.title=c);continue}if(N(v,o)){const e=o.substr(0,4);D(t,n[o],O(a,e));continue}if(!i(n[o]))continue;const{oldTags:r,newTags:l}=K(e,t,o,n[o],O(a,"head"),O(a,"body"));l.length&&(s[o]=l,u[o]=r)}var c;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 E(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=k(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&&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)||E(window.Vue);var S={version:"2.0.5",install:E,hasMetaInfo:c};export default S; +import t from"deepmerge";let e=null;function n(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=10){clearTimeout(e),e=setTimeout(()=>{t()},n)}(()=>t.$meta().refresh())}function o(t){return Array.isArray(t)}function i(t){return void 0===t}function a(t){return"object"==typeof t}function r(t){return"object"==typeof t&&null!==t}function s(t){return"function"==typeof t}function u(t,e){return e&&a(t)?(o(t[e])||(t[e]=[]),t):o(t)?t:[]}function c(t,e,n){u(t,e),t[e].push(n)}function l(t=this){return t&&(!0===t._vueMeta||a(t._vueMeta))}function d(t){if(t.$root._vueMeta.navGuards||!t.$root.$router)return;t.$root._vueMeta.navGuards=!0;const e=t.$root.$router,n=t.$root.$meta();e.beforeEach((t,e,o)=>{n.pause(),o()}),e.afterEach(()=>{const{metaInfo:t}=n.resume();t&&t.afterNavigation&&s(t.afterNavigation)&&t.afterNavigation(t)})}const f=function(){try{return!i(window)}catch(t){return!1}}()?window:global,h=f.console=f.console||{};function p(...t){h&&h.warn&&h.warn(...t)}const m=()=>p("This vue app/component has no vue-meta configuration");let v=1;const y={title:void 0,titleChunk:"",titleTemplate:"%s",htmlAttrs:{},bodyAttrs:{},headAttrs:{},base:[],link:[],meta:[],style:[],script:[],noscript:[],__dangerouslyDisableSanitizers:[],__dangerouslyDisableSanitizersByTagID:{}},$={keyName:"metaInfo",attribute:"data-vue-meta",ssrAttribute:"data-vue-meta-server-rendered",tagIDKeyName:"vmid",contentKeyName:"content",metaTemplateKeyName:"template",ssrAppId:"ssr"},b=["titleChunk","titleTemplate","changed","__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],g=["__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],_=["htmlAttrs","headAttrs","bodyAttrs"],M=["link","style","script"],T=["body","pbody"],N=["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 I(t=!0){return this.$root._vueMeta.paused=!0,()=>A(t)}function A(t=!0){if(this.$root._vueMeta.paused=!1,t)return this.$root.$meta().refresh()}function w({component:t,metaTemplateKeyName:e,contentKeyName:n},o,a,r){return i(a)&&(a=o[e],delete o[e]),!!a&&(i(r)&&(r=o[n]),o[n]=s(a)?a.call(t,r):a.replace(/%s/g,r),!0)}function K(t,e){return t.findIndex(e,arguments[2])}function k(t){return Array.from(t)}function z(t,e){return t.includes(e)}const D=[[/&/g,"&"],[/</g,"<"],[/>/g,">"],[/"/g,'"'],[/'/g,"'"]];function O(e,n,o={}){return n.hasOwnProperty("title")&&void 0===n.title&&delete n.title,_.forEach(t=>{if(n[t])for(const e in n[t])n[t].hasOwnProperty(e)&&void 0===n[t][e]&&(z(N,e)&&p("VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details"),delete n[t][e])}),t(e,n,{arrayMerge:(t,e)=>(function({component:t,tagIDKeyName:e,metaTemplateKeyName:n,contentKeyName:o},i,a){const r=[];return i.forEach((i,s)=>{if(!i[e])return void r.push(i);const u=K(a,t=>t[e]===i[e]),c=a[u];if(-1===u)return void r.push(i);if(c.hasOwnProperty(o)&&void 0===c[o]||c.hasOwnProperty("innerHTML")&&void 0===c.innerHTML)return r.push(i),void a.splice(u,1);if(null===c[o]||null===c.innerHTML)return void a.splice(u,1);const l=i[n];l&&(c[n]?c[o]||w({component:t,metaTemplateKeyName:n,contentKeyName:o},c,void 0,i[o]):w({component:t,metaTemplateKeyName:n,contentKeyName:o},c,l))}),r.concat(a)})(o,t,e)})}function S(t={},e,n={}){const{keyName:o,metaTemplateKeyName:r,tagIDKeyName:u}=t,{$options:c,$children:l}=e;if(e._inactive)return n;if(c[o]){let i=c[o];if(s(i)&&(i=i.call(e)),!a(i))return n;n=O(n,i,t)}return l.length&&l.forEach(e=>{(function(t=this){return t&&!i(t._vueMeta)})(e)&&(n=S(t,e,n))}),r&&n.meta&&(n.meta.forEach(e=>w(t,e)),n.meta=n.meta.filter((t,e,n)=>!t.hasOwnProperty(u)||e===K(n,e=>e[u]===t[u]))),n}function E(t={},e,n=[]){let i=S(t,e,y);i.title&&(i.titleChunk=i.title),i.titleTemplate&&"%s"!==i.titleTemplate&&w({component:e,contentKeyName:"title"},i,i.titleTemplate,i.titleChunk||""),i.base&&(i.base=Object.keys(i.base).length?[i.base]:[]);const a={doEscape:t=>n.reduce((t,[e,n])=>t.replace(e,n),t)};return g.forEach((t,e)=>{if(0===e)u(i,t);else if(1===e)for(const e in i[t])u(i[t],e);a[t]=i[t]}),i=function t(e,n,i){const{tagIDKeyName:a}=n,{doEscape:s=(t=>t),escapeKeys:u}=i,c={};for(const l in e){const d=e[l];if(z(b,l)){c[l]=d;continue}let[f]=g;if(i[f]&&z(i[f],l)){c[l]=d;continue}const h=e[a];if(h&&(f=g[1],i[f]&&i[f][h]&&z(i[f][h],l)))c[l]=d;else if("string"==typeof d?c[l]=s(d):o(d)?c[l]=d.map(e=>r(e)?t(e,n,{...i,escapeKeys:!0}):s(e)):r(d)?c[l]=t(d,n,{...i,escapeKeys:!0}):c[l]=d,u){const t=s(l);l!==t&&(c[t]=c[l],delete c[l])}}return c}(i,t,a)}function j(t,e){return t[e]||(t[e]=document.getElementsByTagName(e)[0]),t[e]}function x({body:t,pbody:e}){return t?"body":e?"pbody":"head"}function P(t,{appId:e,attribute:n,type:o,tagIDKeyName:i},a={}){const r=[`${o}[${n}="${e}"]`,`${o}[data-${i}]`].map(t=>{for(const e in a){const n=a[e];t+=`[data-${e}${n&&!0!==n?`="${n}"`:""}]`}return t});return k(t.querySelectorAll(r.join(", ")))}const C=[];function L(t,e){1===arguments.length&&(e=t,t=""),C.push([t,e])}function H({tagIDKeyName:t},e,n,o){let i=!1;for(const o of n)o[t]&&o.callback&&(i=!0,L(`${e}[data-${t}="${o[t]}"]`,o.callback));return o&&i?B():i}function B(){!function(t=document){return"complete"===t.readyState}()?document.onreadystatechange=()=>{q()}:q()}function q(t){for(const[e,n]of C){const o=`${e}[onload="this.__vm_l=1"]`;let i=[];t||(i=k(document.querySelectorAll(o))),t&&t.matches(o)&&(i=[t]);for(const t of i){if(t.__vm_cb)continue;const e=()=>{t.__vm_cb=!0,t.removeAttribute("onload"),n(t)};t.__vm_l?e():t.__vm_ev||(t.__vm_ev=!0,t.addEventListener("load",e))}}}function V({attribute:t}={},e,n){const i=n.getAttribute(t),a=i?i.split(","):[],r=k(a),s=[];for(const t in e)if(e.hasOwnProperty(t)){const i=z(N,t)?"":o(e[t])?e[t].join(" "):e[t];n.setAttribute(t,i||""),z(a,t)||a.push(t),s.push(r.indexOf(t))}const u=r.filter((t,e)=>!z(s,e)).reduce((t,e)=>(n.removeAttribute(e),t+1),0);a.length===u?n.removeAttribute(t):n.setAttribute(t,a.sort().join(","))}function W(t,e={},n,o,i,a){const{attribute:r,tagIDKeyName:s}=e,u=[s,...T],c=[],l={appId:t,attribute:r,type:n,tagIDKeyName:s},d={head:P(i,l),pbody:P(a,l,{pbody:!0}),body:P(a,l,{body:!0})};if(o.length>1){const t=[];o=o.filter(e=>{const n=JSON.stringify(e),o=!z(t,n);return t.push(n),o})}if(o.length)for(const e of o){if(e.skip)continue;const o=document.createElement(n);o.setAttribute(r,t);for(const t in e){if(!e.hasOwnProperty(t))continue;if("innerHTML"===t){o.innerHTML=e.innerHTML;continue}if("json"===t){o.innerHTML=JSON.stringify(e.json);continue}if("cssText"===t){o.styleSheet?o.styleSheet.cssText=e.cssText:o.appendChild(document.createTextNode(e.cssText));continue}if("callback"===t){o.onload=()=>e[t](o);continue}const n=z(u,t)?`data-${t}`:t,i=z(N,t);if(i&&!e[t])continue;const a=i?"":e[t];o.setAttribute(n,a)}const i=d[x(e)];let a;i.some((t,e)=>(a=e,o.isEqualNode(t)))&&(a||0===a)?i.splice(a,1):c.push(o)}let f=[];for(const t of Object.values(d))f=[...f,...t];for(const t of f)t.parentNode.removeChild(t);for(const t of c)t.hasAttribute("data-body")?a.appendChild(t):t.hasAttribute("data-pbody")?a.insertBefore(t,a.firstChild):i.appendChild(t);return{oldTags:f,newTags:c}}function G(t={}){return function(){const e=E(t,this.$root,D),n=function(t,e={},n){const{ssrAttribute:i,ssrAppId:a}=e,r={},s=j(r,"html");if(t===a&&s.hasAttribute(i)){s.removeAttribute(i);let t=!1;for(const o of M)n[o]&&H(e,o,n[o])&&(t=!0);return t&&B(),!1}const u={},c={};for(const i in n){if(z(b,i))continue;if("title"===i){((l=n.title)||""===l)&&(document.title=l);continue}if(z(_,i)){const t=i.substr(0,4);V(e,n[i],j(r,t));continue}if(!o(n[i]))continue;const{oldTags:a,newTags:s}=W(t,e,i,n[i],j(r,"head"),j(r,"body"));s.length&&(u[i]=s,c[i]=a)}var l;return{addedTags:u,removedTags:c}}(this.$root._vueMeta.appId,t,e);return n&&s(e.changed)&&e.changed(e,n.addedTags,n.removedTags),{vm:this,metaInfo:e,tags:n}}}function J(t,e={}){t.__vuemeta_installed||(t.__vuemeta_installed=!0,e=function(t){t=a(t)?t:{};for(const e in $)t[e]||(t[e]=$[e]);return t}(e),t.prototype.$meta=function(t={}){const e=G(t),n=()=>{};return function(){return this.$root._vueMeta?{getOptions:()=>(function(t){const e={};for(const n in t)e[n]=t[n];return e})(t),refresh:e.bind(this),inject:n,pause:I.bind(this),resume:A.bind(this)}:{getOptions:m,refresh:m,inject:m,pause:m,resume:m}}}(e),t.mixin(function(t,e){const o=["activated","deactivated","beforeMount"];return{beforeCreate(){if(Object.defineProperty(this,"_hasMetaInfo",{configurable:!0,get(){return t.config.devtools&&!this.$root._vueMeta.hasMetaInfoDeprecationWarningShown&&(p("VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead"),this.$root._vueMeta.hasMetaInfoDeprecationWarningShown=!0),l(this)}}),!i(this.$options[e.keyName])&&null!==this.$options[e.keyName]){if(this.$root._vueMeta||(this.$root._vueMeta={appId:v},v++),!this._vueMeta){this._vueMeta=!0;let t=this.$parent;for(;t&&t!==this.$root;)i(t._vueMeta)&&(t._vueMeta=!1),t=t.$parent}s(this.$options[e.keyName])&&(this.$options.computed||(this.$options.computed={}),this.$options.computed.$metaInfo=this.$options[e.keyName],this.$isServer||c(this.$options,"created",()=>{this.$watch("$metaInfo",function(){n(this,"watcher")})})),i(this.$root._vueMeta.initialized)&&(this.$root._vueMeta.initialized=this.$isServer,this.$root._vueMeta.initialized||(c(this.$options,"beforeMount",()=>{this.$root.$el&&this.$root.$el.hasAttribute&&this.$root.$el.hasAttribute("data-server-rendered")&&(this.$root._vueMeta.appId=e.ssrAppId)}),c(this.$options,"mounted",()=>{this.$root._vueMeta.initialized||(this.$root._vueMeta.initializing=!0,this.$nextTick(function(){const{tags:t,metaInfo:o}=this.$root.$meta().refresh();!1===t&&null===this.$root._vueMeta.initialized&&this.$nextTick(()=>n(this,"initializing")),this.$root._vueMeta.initialized=!0,delete this.$root._vueMeta.initializing,!e.refreshOnceOnNavigation&&o.afterNavigation&&d(this)}))}),e.refreshOnceOnNavigation&&d(this))),this.$isServer||(o.forEach(t=>{c(this.$options,t,()=>n(this,t))}),c(this.$options,"destroyed",()=>{const t=setInterval(()=>{this.$el&&null!==this.$el.offsetParent||(clearInterval(t),this.$parent&&n(this,"destroyed"))},50)}))}}}}(t,e)))}i(window)||i(window.Vue)||J(window.Vue);var F={version:"2.1.0",install:J,hasMetaInfo:l};export default F; diff --git a/dist/vue-meta.esm.js b/dist/vue-meta.esm.js index e83fd1a..c19ec99 100644 --- a/dist/vue-meta.esm.js +++ b/dist/vue-meta.esm.js @@ -1,5 +1,5 @@ /** - * vue-meta v2.0.5 + * vue-meta v2.1.0 * (c) 2019 * - Declan de Wet * - Sébastien Chopin (@Atinux) @@ -9,12 +9,11 @@ import deepmerge from 'deepmerge'; -var version = "2.0.5"; +var version = "2.1.0"; // store an id to keep track of DOM updates -var batchId = null; - -function triggerUpdate (vm, hookName) { +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 @@ -24,10 +23,9 @@ function triggerUpdate (vm, hookName) { if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { // batch potential DOM updates to prevent extraneous re-rendering - batchUpdate(function () { return vm.$meta().refresh(); }); + batchUpdate(() => vm.$meta().refresh()); } } - /** * Performs a batched update. * @@ -35,16 +33,13 @@ function triggerUpdate (vm, hookName) { * @param {Function} callback - the update to perform * @return {Number} id - a new ID */ -function batchUpdate (callback, timeout) { - if ( timeout === void 0 ) timeout = 10; +function batchUpdate(callback, timeout = 10) { clearTimeout(batchId); - - batchId = setTimeout(function () { + batchId = setTimeout(() => { callback(); }, timeout); - - return batchId + return batchId; } /** @@ -52,247 +47,254 @@ function batchUpdate (callback, timeout) { * @param {any} arg - the object to check * @return {Boolean} - true if `arg` is an array */ -function isArray (arg) { - return Array.isArray(arg) +function isArray(arg) { + return Array.isArray(arg); +} +function isUndefined(arg) { + return typeof arg === 'undefined'; +} +function isObject(arg) { + return typeof arg === 'object'; +} +function isPureObject(arg) { + return typeof arg === 'object' && arg !== null; +} +function isFunction(arg) { + return typeof arg === 'function'; +} +function isString(arg) { + return typeof arg === 'string'; } -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) { +function ensureIsArray(arg, key) { if (!key || !isObject(arg)) { - return isArray(arg) ? arg : [] + return isArray(arg) ? arg : []; } if (!isArray(arg[key])) { arg[key] = []; } - return arg + + return arg; } - -function ensuredPush (object, key, el) { +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; +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 - return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) +function inMetaInfoBranch(vm = this) { + return vm && !isUndefined(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) { +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 + return; } vm.$root._vueMeta.navGuards = true; - - var $router = vm.$root.$router; - var $meta = vm.$root.$meta(); - - $router.beforeEach(function (to, from, next) { + const $router = vm.$root.$router; + const $meta = vm.$root.$meta(); + $router.beforeEach((to, from, next) => { $meta.pause(); next(); }); + $router.afterEach(() => { + const { + metaInfo + } = $meta.resume(); - $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 hasGlobalWindowFn() { + try { + return !isUndefined(window); + } catch (e) { + return false; + } +} +const hasGlobalWindow = hasGlobalWindowFn(); -function createMixin (Vue, options) { +const _global = hasGlobalWindow ? window : global; + +const console = _global.console = _global.console || {}; +function warn(...args) { + /* istanbul ignore next */ + if (!console || !console.warn) { + return; + } + + console.warn(...args); +} + +let appId = 1; +function createMixin(Vue, options) { // for which Vue lifecycle hooks should the metaInfo be refreshed - var updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; // watch for client side component updates - // watch for client side component updates return { - beforeCreate: function beforeCreate () { - var this$1 = this; - + beforeCreate() { Object.defineProperty(this, '_hasMetaInfo', { configurable: true, - get: function get () { + + 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 + warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; } - return hasMetaInfo(this) - } - }); - // Add a marker to know if it uses metaInfo + 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 }; + this.$root._vueMeta = { + appId + }; appId++; - } - - // to speed up updates we keep track of branches which have a component with vue-meta info defined + } // 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; - 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 + } // 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 () { + ensuredPush(this.$options, 'created', () => { + this.$watch('$metaInfo', function () { triggerUpdate(this, 'watcher'); }); }); } - } - - // force an initial refresh on page load and prevent other lifecycleHooks + } // 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 () { + 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$1.$root.$el && this$1.$root.$el.hasAttribute && this$1.$root.$el.hasAttribute('data-server-rendered')) { - this$1.$root._vueMeta.appId = 'ssr'; + if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) { + this.$root._vueMeta.appId = options.ssrAppId; } - }); + }); // we use the mounted hook here as on page load - // we use the mounted hook here as on page load - ensuredPush(this.$options, 'mounted', function () { - if (!this$1.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'mounted', () => { + if (!this.$root._vueMeta.initialized) { // used in triggerUpdate to check if a change was triggered // during initialization - this$1.$root._vueMeta.initializing = true; + this.$root._vueMeta.initializing = true; // refresh meta in nextTick so all child components have loaded - // 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 + 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(function () { return triggerUpdate(this$1, 'initializing'); }); + 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 + 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 - // add the navigation guards if requested if (options.refreshOnceOnNavigation) { addNavGuards(this); } } - } + } // do not trigger refresh on the server side + - // 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); }); - }); + updateOnLifecycleHook.forEach(lifecycleHook => { + ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook)); + }); // re-render meta data when returning from a child component to parent - // re-render meta data when returning from a child component to parent - ensuredPush(this.$options, 'destroyed', function () { + ensuredPush(this.$options, 'destroyed', () => { // 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) { + const interval = setInterval(() => { + if (this.$el && this.$el.offsetParent !== null) { /* istanbul ignore next line */ - return + return; } clearInterval(interval); - if (!this$1.$parent) { + if (!this.$parent) { /* istanbul ignore next line */ - return + return; } - triggerUpdate(this$1, 'destroyed'); + triggerUpdate(this, 'destroyed'); }, 50); }); } } } - } + + }; } /** * These are constant variables used throughout the application. */ - // set some sane defaults -var defaultInfo = { +const defaultInfo = { title: undefined, titleChunk: '', titleTemplate: '%s', @@ -306,183 +308,112 @@ var defaultInfo = { script: [], noscript: [], __dangerouslyDisableSanitizers: [], - __dangerouslyDisableSanitizersByTagID: {} + __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. + }; - -// 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 +const 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` +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. -var ssrAttribute = 'data-vue-meta-server-rendered'; -// This is the property that tells vue-meta to overwrite (instead of append) +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. -var tagIDKeyName = 'vmid'; -// This is the key name for possible meta templates -var metaTemplateKeyName = 'template'; +const tagIDKeyName = 'vmid'; // This is the key name for possible meta templates -// This is the key name for the content-holding property -var contentKeyName = 'content'; +const metaTemplateKeyName = 'template'; // This is the key name for the content-holding property + +const contentKeyName = 'content'; // The id used for the ssr app + +const ssrAppId = 'ssr'; +const defaultOptions = { + keyName, + attribute, + ssrAttribute, + tagIDKeyName, + contentKeyName, + metaTemplateKeyName, + ssrAppId // List of metaInfo property keys which are configuration options (and dont generate html) -var defaultOptions = { - keyName: keyName, - attribute: attribute, - ssrAttribute: ssrAttribute, - tagIDKeyName: tagIDKeyName, - contentKeyName: contentKeyName, - metaTemplateKeyName: metaTemplateKeyName }; +const metaInfoOptionKeys = ['titleChunk', 'titleTemplate', 'changed', '__dangerouslyDisableSanitizers', '__dangerouslyDisableSanitizersByTagID']; // The metaInfo property keys which are used to disable escaping -// List of metaInfo property keys which are configuration options (and dont generate html) -var metaInfoOptionKeys = [ - 'titleChunk', - 'titleTemplate', - 'changed', - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' -]; +const disableOptionKeys = ['__dangerouslyDisableSanitizers', '__dangerouslyDisableSanitizersByTagID']; // List of metaInfo property keys which only generates attributes and no tags -// The metaInfo property keys which are used to disable escaping -var disableOptionKeys = [ - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' -]; +const metaInfoAttributeKeys = ['htmlAttrs', 'headAttrs', 'bodyAttrs']; // HTML elements which support the onload event -// 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) +const tagsSupportingOnload = ['link', 'style', 'script']; // 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']; +const tagsWithoutEndTag = ['base', 'meta', 'link']; // HTML elements which can have inner content (shortened to our needs) -// Attributes which are inserted as childNodes instead of HTMLAttribute -var tagAttributeAsInnerContent = ['innerHTML', 'cssText']; +const tagsWithInnerContent = ['noscript', 'script', 'style']; // Attributes which are inserted as childNodes instead of HTMLAttribute -// 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' -]; +const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json']; // Attributes which should be added with data- prefix -function setOptions (options) { +const commonDataAttributes = ['body', 'pbody']; // 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']; + +function setOptions(options) { // combine options options = isObject(options) ? options : {}; - for (var key in defaultOptions) { + for (const key in defaultOptions) { if (!options[key]) { options[key] = defaultOptions[key]; } } - return options + return options; } +function getOptions(options) { + const optionsCopy = {}; -function getOptions (options) { - var optionsCopy = {}; - for (var key in options) { + for (const key in options) { optionsCopy[key] = options[key]; } - return optionsCopy + + return optionsCopy; } -function pause (refresh) { - if ( refresh === void 0 ) refresh = true; - +function pause(refresh = true) { this.$root._vueMeta.paused = true; - - return function () { return resume(refresh); } + return () => resume(refresh); } - -function resume (refresh) { - if ( refresh === void 0 ) refresh = true; - +function resume(refresh = true) { this.$root._vueMeta.paused = false; if (refresh) { - return this.$root.$meta().refresh() + return this.$root.$meta().refresh(); } } -function applyTemplate (ref, headObject, template, chunk) { - var component = ref.component; - var metaTemplateKeyName = ref.metaTemplateKeyName; - var contentKeyName = ref.contentKeyName; - +function applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName +}, headObject, template, chunk) { if (isUndefined(template)) { template = headObject[metaTemplateKeyName]; delete headObject[metaTemplateKeyName]; - } + } // return early if no template defined + - // return early if no template defined if (!template) { - return false + return false; } if (isUndefined(chunk)) { chunk = headObject[contentKeyName]; } - headObject[contentKeyName] = isFunction(template) - ? template.call(component, chunk) - : template.replace(/%s/g, chunk); - - return true + headObject[contentKeyName] = isFunction(template) ? template.call(component, chunk) : template.replace(/%s/g, chunk); + return true; } /* @@ -493,179 +424,185 @@ function applyTemplate (ref, headObject, template, chunk) { * 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; - +function findIndex(array, predicate) { 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 + for (let idx = 0; idx < array.length; idx++) { + if (predicate.call(arguments[2], array[idx], idx, array)) { + return idx; } } - return -1 - } - return array.findIndex(predicate, arguments[2]) -} -function toArray (arg) { + return -1; + } + + return array.findIndex(predicate, arguments[2]); +} +function toArray(arg) { if ( !Array.from) { - return Array.prototype.slice.call(arg) + return Array.prototype.slice.call(arg); } - return Array.from(arg) -} -function includes (array, value) { + return Array.from(arg); +} +function includes(array, value) { if ( !Array.prototype.includes) { - for (var idx in array) { + for (const idx in array) { if (array[idx] === value) { - return true + return true; } } - return false + return false; } - return array.includes(value) + + return array.includes(value); } -var serverSequences = [ - [/&/g, '&'], - [/</g, '<'], - [/>/g, '>'], - [/"/g, '"'], - [/'/g, '''] -]; +const serverSequences = [[/&/g, '&'], [/</g, '<'], [/>/g, '>'], [/"/g, '"'], [/'/g, ''']]; +const clientSequences = [[/&/g, '\u0026'], [/</g, '\u003C'], [/>/g, '\u003E'], [/"/g, '\u0022'], [/'/g, '\u0027']]; // sanitizes potentially dangerous characters -var clientSequences = [ - [/&/g, '\u0026'], - [/</g, '\u003C'], - [/>/g, '\u003E'], - [/"/g, '\u0022'], - [/'/g, '\u0027'] -]; +function escape(info, options, escapeOptions) { + const { + tagIDKeyName + } = options; + const { + doEscape = v => v, + escapeKeys + } = escapeOptions; + const escaped = {}; -// 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 (const key in info) { + const value = info[key]; // no need to escape configuration options - for (var key in info) { - var value = info[key]; - - // no need to escape configuration options if (includes(metaInfoOptionKeys, key)) { escaped[key] = value; - continue + continue; } - var disableKey = disableOptionKeys[0]; + 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 + continue; } - var tagId = info[tagIDKeyName]; + const tagId = info[tagIDKeyName]; + if (tagId) { - disableKey = disableOptionKeys[1]; + disableKey = disableOptionKeys[1]; // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped - // 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 + 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) + escaped[key] = value.map(v => { + if (isPureObject(v)) { + return escape(v, options, { ...escapeOptions, + escapeKeys: true + }); + } + + return doEscape(v); + }); + } else if (isPureObject(value)) { + escaped[key] = escape(value, options, { ...escapeOptions, + escapeKeys: true }); - } else if (isObject(value)) { - escaped[key] = escape(value, options, escapeOptions); } else { escaped[key] = value; } + + if (escapeKeys) { + const escapedKey = doEscape(key); + + if (key !== escapedKey) { + escaped[escapedKey] = escaped[key]; + delete escaped[key]; + } + } } - return escaped + return escaped; } -function arrayMerge (ref, target, source) { - var component = ref.component; - var tagIDKeyName = ref.tagIDKeyName; - var metaTemplateKeyName = ref.metaTemplateKeyName; - var contentKeyName = ref.contentKeyName; - +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 - var destination = []; - - target.forEach(function (targetItem, targetIndex) { + const destination = []; + target.forEach((targetItem, targetIndex) => { // no tagID so no need to check for duplicity if (!targetItem[tagIDKeyName]) { destination.push(targetItem); - return + return; } - var sourceIndex = findIndex(source, function (item) { return item[tagIDKeyName] === targetItem[tagIDKeyName]; }); - var sourceItem = source[sourceIndex]; + const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName]); + const sourceItem = source[sourceIndex]; // source doesnt contain any duplicate vmid's, we can keep targetItem - // 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 + 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 + + + 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 - + 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 - } + return; + } // now we only need to check if the target has a template to combine it with the source + + + const targetTemplate = targetItem[metaTemplateKeyName]; - // 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 + return; } - var sourceTemplate = sourceItem[metaTemplateKeyName]; + const sourceTemplate = sourceItem[metaTemplateKeyName]; if (!sourceTemplate) { // use parent template and child content - applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, targetTemplate); + applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, sourceItem, targetTemplate); } else if (!sourceItem[contentKeyName]) { // use child template and parent content - applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, sourceItem, undefined, targetItem[contentKeyName]); } }); - - return destination.concat(source) + return destination.concat(source); } - -function merge (target, source, options) { - if ( options === void 0 ) options = {}; - +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) @@ -673,25 +610,24 @@ function merge (target, source, options) { delete source.title; } - metaInfoAttributeKeys.forEach(function (attrKey) { + metaInfoAttributeKeys.forEach(attrKey => { if (!source[attrKey]) { - return + return; } - for (var key in source[attrKey]) { + for (const key in source[attrKey]) { if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { - if (booleanHtmlAttributes.includes(key)) { - // eslint-disable-next-line no-console - console.warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); + if (includes(booleanHtmlAttributes, key)) { + warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); } + delete source[attrKey][key]; } } }); - return deepmerge(target, source, { - arrayMerge: function (t, s) { return arrayMerge(options, t, s); } - }) + arrayMerge: (t, s) => arrayMerge(options, t, s) + }); } /** @@ -708,45 +644,46 @@ function merge (target, source, options) { * @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; +function getComponentOption(options = {}, component, result = {}) { + const { + keyName, + metaTemplateKeyName, + tagIDKeyName + } = options; + const { + $options, + $children + } = component; if (component._inactive) { - return result - } + return result; + } // only collect option data if it exists + - // only collect option data if it exists if ($options[keyName]) { - var data = $options[keyName]; + let data = $options[keyName]; // if option is a function, replace it with it's result - // 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 + - // ignore data if its not an object, then we keep our previous result if (!isObject(data)) { - return result - } + return result; + } // merge with existing options + - // merge with existing options result = merge(result, data, options); - } + } // collect & aggregate child options if deep = true + - // collect & aggregate child options if deep = true if ($children.length) { - $children.forEach(function (childComponent) { + $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 + return; } result = getComponentOption(options, childComponent, result); @@ -755,20 +692,17 @@ function getComponentOption (options, component, result) { if (metaTemplateKeyName && result.meta) { // apply templates if needed - result.meta.forEach(function (metaObject) { return applyTemplate(options, metaObject); }); + result.meta.forEach(metaObject => applyTemplate(options, metaObject)); // remove meta items with duplicate vmid's - // 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]; }) - ) + 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 + return result; } /** @@ -778,56 +712,180 @@ function getComponentOption (options, component, result) { * @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 = []; +function getMetaInfo(options = {}, component, escapeSequences = []) { // collect & aggregate all metaInfo $options - var info = getComponentOption(options, component, defaultInfo); - - // Remove all "template" tags from meta - + 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 + - // 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 + 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] : []; } - 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); } + const escapeOptions = { + doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value) }; - - disableOptionKeys.forEach(function (disableKey, index) { + disableOptionKeys.forEach((disableKey, index) => { if (index === 0) { ensureIsArray(info, disableKey); } else if (index === 1) { - for (var key in info[disableKey]) { + for (const key in info[disableKey]) { ensureIsArray(info[disableKey], key); } } escapeOptions[disableKey] = info[disableKey]; - }); + }); // begin sanitization - // begin sanitization info = escape(info, options, escapeOptions); + return info; +} - return info +function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag]; +} +function getElementsKey({ + body, + pbody +}) { + return body ? 'body' : pbody ? 'pbody' : 'head'; +} +function queryElements(parentNode, { + appId, + attribute, + type, + tagIDKeyName +}, attributes = {}) { + const queries = [`${type}[${attribute}="${appId}"]`, `${type}[data-${tagIDKeyName}]`].map(query => { + for (const key in attributes) { + const val = attributes[key]; + const attributeValue = val && val !== true ? `="${val}"` : ''; + query += `[data-${key}${attributeValue}]`; + } + + return query; + }); + return toArray(parentNode.querySelectorAll(queries.join(', '))); +} + +const callbacks = []; +function isDOMComplete(d = document) { + return d.readyState === 'complete'; +} +function addCallback(query, callback) { + if (arguments.length === 1) { + callback = query; + query = ''; + } + + callbacks.push([query, callback]); +} +function addCallbacks({ + tagIDKeyName +}, type, tags, autoAddListeners) { + let hasAsyncCallback = false; + + for (const tag of tags) { + if (!tag[tagIDKeyName] || !tag.callback) { + continue; + } + + hasAsyncCallback = true; + addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback); + } + + if (!autoAddListeners || !hasAsyncCallback) { + return hasAsyncCallback; + } + + return addListeners(); +} +function addListeners() { + if (isDOMComplete()) { + applyCallbacks(); + return; + } // Instead of using a MutationObserver, we just apply + + /* istanbul ignore next */ + + + document.onreadystatechange = () => { + applyCallbacks(); + }; +} +function applyCallbacks(matchElement) { + for (const [query, callback] of callbacks) { + const selector = `${query}[onload="this.__vm_l=1"]`; + let elements = []; + + if (!matchElement) { + elements = toArray(document.querySelectorAll(selector)); + } + + if (matchElement && matchElement.matches(selector)) { + elements = [matchElement]; + } + + for (const element of elements) { + /* __vm_cb: whether the load callback has been called + * __vm_l: set by onload attribute, whether the element was loaded + * __vm_ev: whether the event listener was added or not + */ + if (element.__vm_cb) { + continue; + } + + const onload = () => { + /* Mark that the callback for this element has already been called, + * this prevents the callback to run twice in some (rare) conditions + */ + element.__vm_cb = true; + /* onload needs to be removed because we only need the + * attribute after ssr and if we dont remove it the node + * will fail isEqualNode on the client + */ + + element.removeAttribute('onload'); + callback(element); + }; + /* IE9 doesnt seem to load scripts synchronously, + * causing a script sometimes/often already to be loaded + * when we add the event listener below (thus adding an onload event + * listener has no use because it will never be triggered). + * Therefore we add the onload attribute during ssr, and + * check here if it was already loaded or not + */ + + + if (element.__vm_l) { + onload(); + continue; + } + + if (!element.__vm_ev) { + element.__vm_ev = true; + element.addEventListener('load', onload); + } + } + } } /** @@ -836,43 +894,38 @@ function getMetaInfo (options, component, escapeSequences) { * @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); +function updateAttribute({ + attribute +} = {}, attrs, tag) { + const vueMetaAttrString = tag.getAttribute(attribute); + const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + const toRemove = toArray(vueMetaAttrs); + const keepIndexes = []; - var keepIndexes = []; - for (var attr in attrs) { + for (const attr in attrs) { if (attrs.hasOwnProperty(attr)) { - var value = includes(booleanHtmlAttributes, attr) - ? '' - : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[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 + - // 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); + 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(',')); + tag.setAttribute(attribute, vueMetaAttrs.sort().join(',')); } } @@ -881,9 +934,9 @@ function updateAttribute (ref, attrs, tag) { * * @param {String} title - the new title of the document */ -function updateTitle (title) { - if (title === undefined) { - return +function updateTitle(title) { + if (!title && title !== '') { + return; } document.title = title; @@ -897,98 +950,143 @@ function updateTitle (title) { * @param {(Array<Object>|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 = []; +function updateTag(appId, options = {}, type, tags, head, body) { + const { + attribute, + tagIDKeyName + } = options; + const dataAttributes = [tagIDKeyName, ...commonDataAttributes]; + const newElements = []; + const queryOptions = { + appId, + attribute, + type, + tagIDKeyName + }; + const currentElements = { + head: queryElements(head, queryOptions), + pbody: queryElements(body, queryOptions, { + pbody: true + }), + body: queryElements(body, queryOptions, { + body: true + }) + }; 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); + const found = []; + tags = tags.filter(x => { + const k = JSON.stringify(x); + const res = !includes(found, k); found.push(k); - return res + 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 isBooleanAttribute = includes(booleanHtmlAttributes, attr); - if (isBooleanAttribute && !tag[attr]) { - continue - } - - var value = isBooleanAttribute ? '' : tag[attr]; - newElement.setAttribute(_attr, value); - } - } + for (const tag of tags) { + if (tag.skip) { + continue; } - // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. - var indexToDelete; - var hasEqualElement = oldTags.some(function (existingTag, index) { + const newElement = document.createElement(type); + newElement.setAttribute(attribute, appId); + + for (const attr in tag) { + /* istanbul ignore next */ + if (!tag.hasOwnProperty(attr)) { + continue; + } + + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + continue; + } + + if (attr === 'json') { + newElement.innerHTML = JSON.stringify(tag.json); + continue; + } + + if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + + continue; + } + + if (attr === 'callback') { + newElement.onload = () => tag[attr](newElement); + + continue; + } + + const _attr = includes(dataAttributes, attr) ? `data-${attr}` : attr; + + const isBooleanAttribute = includes(booleanHtmlAttributes, attr); + + if (isBooleanAttribute && !tag[attr]) { + continue; + } + + const value = isBooleanAttribute ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + + const oldElements = currentElements[getElementsKey(tag)]; // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + + let indexToDelete; + const hasEqualElement = oldElements.some((existingTag, index) => { indexToDelete = index; - return newElement.isEqualNode(existingTag) + return newElement.isEqualNode(existingTag); }); if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { - oldTags.splice(indexToDelete, 1); + oldElements.splice(indexToDelete, 1); } else { - newTags.push(newElement); + newElements.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] + let oldElements = []; + + for (const current of Object.values(currentElements)) { + oldElements = [...oldElements, ...current]; + } // remove old elements + + + for (const element of oldElements) { + element.parentNode.removeChild(element); + } // insert new elements + + + for (const element of newElements) { + if (element.hasAttribute('data-body')) { + body.appendChild(element); + continue; + } + + if (element.hasAttribute('data-pbody')) { + body.insertBefore(element, body.firstChild); + continue; + } + + head.appendChild(element); + } + + return { + oldTags: oldElements, + newTags: newElements + }; } /** @@ -996,60 +1094,66 @@ function getTag (tags, tag) { * * @param {Object} newInfo - the meta info to update to */ -function updateClientMetaInfo (appId, options, newInfo) { - if ( options === void 0 ) options = {}; - var ssrAttribute = options.ssrAttribute; +function updateClientMetaInfo(appId, options = {}, newInfo) { + const { + ssrAttribute, + ssrAppId + } = options; // only cache tags for current update - // only cache tags for current update - var tags = {}; + const tags = {}; + const htmlTag = getTag(tags, 'html'); // if this is a server render, then dont update - var htmlTag = getTag(tags, 'html'); - - // if this is a server render, then dont update - if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) { // remove the server render attribute so we can update on (next) changes - htmlTag.removeAttribute(ssrAttribute); - return false - } + htmlTag.removeAttribute(ssrAttribute); // add load callbacks if the - // initialize tracked changes - var addedTags = {}; - var removedTags = {}; + let addLoadListeners = false; - for (var type in newInfo) { + for (const type of tagsSupportingOnload) { + if (newInfo[type] && addCallbacks(options, type, newInfo[type])) { + addLoadListeners = true; + } + } + + if (addLoadListeners) { + addListeners(); + } + + return false; + } // initialize tracked changes + + + const addedTags = {}; + const removedTags = {}; + + for (const type in newInfo) { // ignore these if (includes(metaInfoOptionKeys, type)) { - continue + continue; } if (type === 'title') { // update the title updateTitle(newInfo.title); - continue + continue; } if (includes(metaInfoAttributeKeys, type)) { - var tagName = type.substr(0, 4); + const tagName = type.substr(0, 4); updateAttribute(options, newInfo[type], getTag(tags, tagName)); - continue - } + continue; + } // tags should always be an array, ignore if it isnt + - // tags should always be an array, ignore if it isnt if (!isArray(newInfo[type])) { - continue + continue; } - var ref = updateTag( - appId, - options, - type, - newInfo[type], - getTag(tags, 'head'), - getTag(tags, 'body') - ); - var oldTags = ref.oldTags; - var newTags = ref.newTags; + const { + oldTags, + newTags + } = updateTag(appId, options, type, newInfo[type], getTag(tags, 'head'), getTag(tags, 'body')); if (newTags.length) { addedTags[type] = newTags; @@ -1057,12 +1161,13 @@ function updateClientMetaInfo (appId, options, newInfo) { } } - return { addedTags: addedTags, removedTags: removedTags } + return { + addedTags, + removedTags + }; } -function _refresh (options) { - if ( options === void 0 ) options = {}; - +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 @@ -1073,18 +1178,21 @@ function _refresh (options) { * * @return {Object} - new meta info */ - return function refresh () { - var metaInfo = getMetaInfo(options, this.$root, clientSequences); + 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 - 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 } - } + return { + vm: this, + metaInfo, + tags + }; + }; } /** @@ -1094,31 +1202,36 @@ function _refresh (options) { * @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; +function attributeGenerator({ + attribute, + ssrAttribute +} = {}, type, data) { return { - text: function text () { - var attributeStr = ''; - var watchedAttrs = []; + text(addSrrAttribute) { + let attributeStr = ''; + const watchedAttrs = []; - for (var attr in data) { + for (const 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 += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) ? attr : `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"`; attributeStr += ' '; } } - attributeStr += attribute + "=\"" + ((watchedAttrs.sort()).join(',')) + "\""; - return attributeStr + if (attributeStr) { + attributeStr += `${attribute}="${watchedAttrs.sort().join(',')}"`; + } + + if (type === 'htmlAttrs' && addSrrAttribute) { + return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}`; + } + + return attributeStr; } - } + + }; } /** @@ -1128,15 +1241,19 @@ function attributeGenerator (ref, type, data) { * @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; - +function titleGenerator({ + attribute +} = {}, type, data) { return { - text: function text () { - return ("<" + type + ">" + data + "</" + type + ">") + text() { + if (!data) { + return ''; + } + + return `<${type}>${data}</${type}>`; } - } + + }; } /** @@ -1146,72 +1263,82 @@ function titleGenerator (appId, ref, type, data) { * @param {(Array<Object>|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; +function tagGenerator({ + ssrAppId, + attribute, + tagIDKeyName +} = {}, type, tags) { + const dataAttributes = [tagIDKeyName, 'callback', ...commonDataAttributes]; return { - text: function text (ref) { - if ( ref === void 0 ) ref = {}; - var body = ref.body; if ( body === void 0 ) body = false; - + text({ + body = false, + pbody = false + } = {}) { // build a string containing all tags of this type - return tags.reduce(function (tagsStr, tag) { - var tagKeys = Object.keys(tag); + return tags.reduce((tagsStr, tag) => { + if (tag.skip) { + return tagsStr; + } + + const tagKeys = Object.keys(tag); if (tagKeys.length === 0) { - return tagsStr // Bail on empty tag object + return tagsStr; // Bail on empty tag object } - if (Boolean(tag.body) !== body) { - return tagsStr + if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) { + return tagsStr; } - // build a string containing all attributes of this tag - var attrs = tagKeys.reduce(function (attrsStr, attr) { + let attrs = tag.once ? '' : ` ${attribute}="${ssrAppId}"`; // build a string containing all attributes of this tag + + for (const attr in tag) { // these attributes are treated as children on the tag if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') { - return attrsStr - } + continue; + } // these form the attribute list for this tag - // these form the attribute list for this tag - var prefix = ''; - if ([tagIDKeyName, 'body'].includes(attr)) { + + let prefix = ''; + + if (dataAttributes.includes(attr)) { prefix = 'data-'; } - var isBooleanAttr = booleanHtmlAttributes.includes(attr); - if (isBooleanAttr && !tag[attr]) { - return attrsStr + if (attr === 'callback') { + attrs += ` onload="this.__vm_l=1"`; + continue; } - return isBooleanAttr - ? (attrsStr + " " + prefix + attr) - : (attrsStr + " " + prefix + attr + "=\"" + (tag[attr]) + "\"") - }, ''); + const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr); - // grab child content from one of these attributes, if possible - var content = tag.innerHTML || tag.cssText || ''; + if (isBooleanAttr && !tag[attr]) { + continue; + } - // generate tag exactly without any other redundant attribute - var observeTag = tag.once - ? '' - : (attribute + "=\"" + appId + "\""); + attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`); + } + let json = ''; + + if (tag.json) { + json = JSON.stringify(tag.json); + } // grab child content from one of these attributes, if possible + + + const content = tag.innerHTML || tag.cssText || json; // generate tag exactly without any other redundant attribute // these tags have no end tag - var hasEndTag = !tagsWithoutEndTag.includes(type); - // these tag types will have content inserted - var hasContent = hasEndTag && tagsWithInnerContent.includes(type); + const hasEndTag = !tagsWithoutEndTag.includes(type); // these tag types will have content inserted - // the final string for this specific tag - return !hasContent - ? (tagsStr + "<" + type + " " + observeTag + attrs + (hasEndTag ? '/' : '') + ">") - : (tagsStr + "<" + type + " " + observeTag + attrs + ">" + content + "</" + type + ">") - }, '') + const hasContent = hasEndTag && tagsWithInnerContent.includes(type); // the final string for this specific tag + + return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` + (hasContent ? `${content}</${type}>` : ''); + }, ''); } - } + + }; } /** @@ -1222,21 +1349,19 @@ function tagGenerator (appId, ref, type, tags) { * @return {Object} - the new injector */ -function generateServerInjector (appId, options, type, data) { +function generateServerInjector(options, type, data) { if (type === 'title') { - return titleGenerator(appId, options, type, data) + return titleGenerator(options, type, data); } if (metaInfoAttributeKeys.includes(type)) { - return attributeGenerator(options, type, data) + return attributeGenerator(options, type, data); } - return tagGenerator(appId, options, type, data) + return tagGenerator(options, type, data); } -function _inject (options) { - if ( options === void 0 ) options = {}; - +function _inject(options = {}) { /** * Converts the state of the meta info object such that each item * can be compiled to a tag string on the server @@ -1244,66 +1369,62 @@ function _inject (options) { * @this {Object} - Vue instance - ideally the root component * @return {Object} - server meta info with `toString` methods */ - return function inject () { + return function inject() { // get meta info with sensible defaults - var metaInfo = getMetaInfo(options, this.$root, serverSequences); + const metaInfo = getMetaInfo(options, this.$root, serverSequences); // generate server injectors - // generate server injectors - for (var key in metaInfo) { + for (const key in metaInfo) { if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) { - metaInfo[key] = generateServerInjector('ssr', options, key, metaInfo[key]); + metaInfo[key] = generateServerInjector(options, key, metaInfo[key]); } } - return metaInfo - } + return metaInfo; + }; } -function _$meta (options) { - if ( options === void 0 ) options = {}; - - var _refresh$1 = _refresh(options); - var _inject$1 = _inject(options); +function _$meta(options = {}) { + const _refresh$1 = _refresh(options); + const _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 function $meta() { return { - getOptions: function () { return getOptions(options); }, + getOptions: () => 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 = {}; +function install(Vue, options = {}) { if (Vue.__vuemeta_installed) { - return + 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 + version, + install, + hasMetaInfo }; export default index; diff --git a/dist/vue-meta.js b/dist/vue-meta.js index a255c38..3341bb1 100644 --- a/dist/vue-meta.js +++ b/dist/vue-meta.js @@ -1,5 +1,5 @@ /** - * vue-meta v2.0.5 + * vue-meta v2.1.0 * (c) 2019 * - Declan de Wet * - Sébastien Chopin (@Atinux) @@ -13,12 +13,11 @@ (global = global || self, global.VueMeta = factory()); }(this, function () { 'use strict'; - var version = "2.0.5"; + var version = "2.1.0"; // store an id to keep track of DOM updates - var batchId = null; - - function triggerUpdate (vm, hookName) { + 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 @@ -28,10 +27,9 @@ if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { // batch potential DOM updates to prevent extraneous re-rendering - batchUpdate(function () { return vm.$meta().refresh(); }); + batchUpdate(() => vm.$meta().refresh()); } } - /** * Performs a batched update. * @@ -39,16 +37,13 @@ * @param {Function} callback - the update to perform * @return {Number} id - a new ID */ - function batchUpdate (callback, timeout) { - if ( timeout === void 0 ) timeout = 10; + function batchUpdate(callback, timeout = 10) { clearTimeout(batchId); - - batchId = setTimeout(function () { + batchId = setTimeout(() => { callback(); }, timeout); - - return batchId + return batchId; } /** @@ -56,247 +51,255 @@ * @param {any} arg - the object to check * @return {Boolean} - true if `arg` is an array */ - function isArray (arg) { - return Array.isArray(arg) + function isArray(arg) { + return Array.isArray(arg); + } + function isUndefined(arg) { + return typeof arg === 'undefined'; + } + function isObject(arg) { + return typeof arg === 'object'; + } + function isPureObject(arg) { + return typeof arg === 'object' && arg !== null; + } + function isFunction(arg) { + return typeof arg === 'function'; + } + function isString(arg) { + return typeof arg === 'string'; } - 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) { + function ensureIsArray(arg, key) { if (!key || !isObject(arg)) { - return isArray(arg) ? arg : [] + return isArray(arg) ? arg : []; } if (!isArray(arg[key])) { arg[key] = []; } - return arg + + return arg; } - - function ensuredPush (object, key, el) { + 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; + 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 - return vm && (vm._vueMeta === true || isObject(vm._vueMeta)) + function inMetaInfoBranch(vm = this) { + return vm && !isUndefined(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) { + 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 + return; } vm.$root._vueMeta.navGuards = true; - - var $router = vm.$root.$router; - var $meta = vm.$root.$meta(); - - $router.beforeEach(function (to, from, next) { + const $router = vm.$root.$router; + const $meta = vm.$root.$meta(); + $router.beforeEach((to, from, next) => { $meta.pause(); next(); }); + $router.afterEach(() => { + const { + metaInfo + } = $meta.resume(); - $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 hasGlobalWindowFn() { + try { + return !isUndefined(window); + } catch (e) { + return false; + } + } + const hasGlobalWindow = hasGlobalWindowFn(); - function createMixin (Vue, options) { + const _global = hasGlobalWindow ? window : global; + + const console = _global.console = _global.console || {}; + function warn(...args) { + /* istanbul ignore next */ + if (!console || !console.warn) { + return; + } + + console.warn(...args); + } + const showWarningNotSupported = () => warn('This vue app/component has no vue-meta configuration'); + + let appId = 1; + function createMixin(Vue, options) { // for which Vue lifecycle hooks should the metaInfo be refreshed - var updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; + const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']; // watch for client side component updates - // watch for client side component updates return { - beforeCreate: function beforeCreate () { - var this$1 = this; - + beforeCreate() { Object.defineProperty(this, '_hasMetaInfo', { configurable: true, - get: function get () { + + 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 + warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead'); this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true; } - return hasMetaInfo(this) - } - }); - // Add a marker to know if it uses metaInfo + 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 }; + this.$root._vueMeta = { + appId + }; appId++; - } - - // to speed up updates we keep track of branches which have a component with vue-meta info defined + } // 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; - 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 + } // 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 () { + ensuredPush(this.$options, 'created', () => { + this.$watch('$metaInfo', function () { triggerUpdate(this, 'watcher'); }); }); } - } - - // force an initial refresh on page load and prevent other lifecycleHooks + } // 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 () { + 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$1.$root.$el && this$1.$root.$el.hasAttribute && this$1.$root.$el.hasAttribute('data-server-rendered')) { - this$1.$root._vueMeta.appId = 'ssr'; + if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) { + this.$root._vueMeta.appId = options.ssrAppId; } - }); + }); // we use the mounted hook here as on page load - // we use the mounted hook here as on page load - ensuredPush(this.$options, 'mounted', function () { - if (!this$1.$root._vueMeta.initialized) { + ensuredPush(this.$options, 'mounted', () => { + if (!this.$root._vueMeta.initialized) { // used in triggerUpdate to check if a change was triggered // during initialization - this$1.$root._vueMeta.initializing = true; + this.$root._vueMeta.initializing = true; // refresh meta in nextTick so all child components have loaded - // 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 + 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(function () { return triggerUpdate(this$1, 'initializing'); }); + 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 + 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 - // add the navigation guards if requested if (options.refreshOnceOnNavigation) { addNavGuards(this); } } - } + } // do not trigger refresh on the server side + - // 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); }); - }); + updateOnLifecycleHook.forEach(lifecycleHook => { + ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook)); + }); // re-render meta data when returning from a child component to parent - // re-render meta data when returning from a child component to parent - ensuredPush(this.$options, 'destroyed', function () { + ensuredPush(this.$options, 'destroyed', () => { // 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) { + const interval = setInterval(() => { + if (this.$el && this.$el.offsetParent !== null) { /* istanbul ignore next line */ - return + return; } clearInterval(interval); - if (!this$1.$parent) { + if (!this.$parent) { /* istanbul ignore next line */ - return + return; } - triggerUpdate(this$1, 'destroyed'); + triggerUpdate(this, 'destroyed'); }, 50); }); } } } - } + + }; } /** * These are constant variables used throughout the application. */ - // set some sane defaults - var defaultInfo = { + const defaultInfo = { title: undefined, titleChunk: '', titleTemplate: '%s', @@ -310,176 +313,105 @@ script: [], noscript: [], __dangerouslyDisableSanitizers: [], - __dangerouslyDisableSanitizersByTagID: {} + __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. + }; - - // 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 + const 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` + 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. - var ssrAttribute = 'data-vue-meta-server-rendered'; - // This is the property that tells vue-meta to overwrite (instead of append) + 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. - var tagIDKeyName = 'vmid'; - // This is the key name for possible meta templates - var metaTemplateKeyName = 'template'; + const tagIDKeyName = 'vmid'; // This is the key name for possible meta templates - // This is the key name for the content-holding property - var contentKeyName = 'content'; + const metaTemplateKeyName = 'template'; // This is the key name for the content-holding property + + const contentKeyName = 'content'; // The id used for the ssr app + + const ssrAppId = 'ssr'; + const defaultOptions = { + keyName, + attribute, + ssrAttribute, + tagIDKeyName, + contentKeyName, + metaTemplateKeyName, + ssrAppId // List of metaInfo property keys which are configuration options (and dont generate html) - var defaultOptions = { - keyName: keyName, - attribute: attribute, - ssrAttribute: ssrAttribute, - tagIDKeyName: tagIDKeyName, - contentKeyName: contentKeyName, - metaTemplateKeyName: metaTemplateKeyName }; + const metaInfoOptionKeys = ['titleChunk', 'titleTemplate', 'changed', '__dangerouslyDisableSanitizers', '__dangerouslyDisableSanitizersByTagID']; // The metaInfo property keys which are used to disable escaping - // List of metaInfo property keys which are configuration options (and dont generate html) - var metaInfoOptionKeys = [ - 'titleChunk', - 'titleTemplate', - 'changed', - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' - ]; + const disableOptionKeys = ['__dangerouslyDisableSanitizers', '__dangerouslyDisableSanitizersByTagID']; // List of metaInfo property keys which only generates attributes and no tags - // The metaInfo property keys which are used to disable escaping - var disableOptionKeys = [ - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' - ]; + const metaInfoAttributeKeys = ['htmlAttrs', 'headAttrs', 'bodyAttrs']; // HTML elements which support the onload event - // List of metaInfo property keys which only generates attributes and no tags - var metaInfoAttributeKeys = [ - 'htmlAttrs', - 'headAttrs', - 'bodyAttrs' - ]; + const tagsSupportingOnload = ['link', 'style', 'script']; // HTML elements which dont have a head tag (shortened to our needs) - // 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' - ]; + const commonDataAttributes = ['body', 'pbody']; // from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202 - // eslint-disable-next-line no-console - var showWarningNotSupported = function () { return console.warn('This vue app/component has no vue-meta configuration'); }; + 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']; - function setOptions (options) { + function setOptions(options) { // combine options options = isObject(options) ? options : {}; - for (var key in defaultOptions) { + for (const key in defaultOptions) { if (!options[key]) { options[key] = defaultOptions[key]; } } - return options + return options; } + function getOptions(options) { + const optionsCopy = {}; - function getOptions (options) { - var optionsCopy = {}; - for (var key in options) { + for (const key in options) { optionsCopy[key] = options[key]; } - return optionsCopy + + return optionsCopy; } - function pause (refresh) { - if ( refresh === void 0 ) refresh = true; - + function pause(refresh = true) { this.$root._vueMeta.paused = true; - - return function () { return resume(refresh); } + return () => resume(refresh); } - - function resume (refresh) { - if ( refresh === void 0 ) refresh = true; - + function resume(refresh = true) { this.$root._vueMeta.paused = false; if (refresh) { - return this.$root.$meta().refresh() + return this.$root.$meta().refresh(); } } - function applyTemplate (ref, headObject, template, chunk) { - var component = ref.component; - var metaTemplateKeyName = ref.metaTemplateKeyName; - var contentKeyName = ref.contentKeyName; - + function applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, headObject, template, chunk) { if (isUndefined(template)) { template = headObject[metaTemplateKeyName]; delete headObject[metaTemplateKeyName]; - } + } // return early if no template defined + - // return early if no template defined if (!template) { - return false + return false; } if (isUndefined(chunk)) { chunk = headObject[contentKeyName]; } - headObject[contentKeyName] = isFunction(template) - ? template.call(component, chunk) - : template.replace(/%s/g, chunk); - - return true + headObject[contentKeyName] = isFunction(template) ? template.call(component, chunk) : template.replace(/%s/g, chunk); + return true; } /* @@ -490,281 +422,287 @@ * 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; - + function findIndex(array, predicate) { 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 + for (let idx = 0; idx < array.length; idx++) { + if (predicate.call(arguments[2], array[idx], idx, array)) { + return idx; } } - return -1 - } - return array.findIndex(predicate, arguments[2]) - } - function toArray (arg) { + return -1; + } + + return array.findIndex(predicate, arguments[2]); + } + function toArray(arg) { if ( !Array.from) { - return Array.prototype.slice.call(arg) + return Array.prototype.slice.call(arg); } - return Array.from(arg) - } - function includes (array, value) { + return Array.from(arg); + } + function includes(array, value) { if ( !Array.prototype.includes) { - for (var idx in array) { + for (const idx in array) { if (array[idx] === value) { - return true + return true; } } - return false + return false; } - return array.includes(value) + + return array.includes(value); } - var clientSequences = [ - [/&/g, '\u0026'], - [/</g, '\u003C'], - [/>/g, '\u003E'], - [/"/g, '\u0022'], - [/'/g, '\u0027'] - ]; + const clientSequences = [[/&/g, '\u0026'], [/</g, '\u003C'], [/>/g, '\u003E'], [/"/g, '\u0022'], [/'/g, '\u0027']]; // sanitizes potentially dangerous characters - // 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 = {}; + function escape(info, options, escapeOptions) { + const { + tagIDKeyName + } = options; + const { + doEscape = v => v, + escapeKeys + } = escapeOptions; + const escaped = {}; - for (var key in info) { - var value = info[key]; + for (const key in info) { + const value = info[key]; // no need to escape configuration options - // no need to escape configuration options if (includes(metaInfoOptionKeys, key)) { escaped[key] = value; - continue + continue; } - var disableKey = disableOptionKeys[0]; + 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 + continue; } - var tagId = info[tagIDKeyName]; + const tagId = info[tagIDKeyName]; + if (tagId) { - disableKey = disableOptionKeys[1]; + disableKey = disableOptionKeys[1]; // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped - // 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 + 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) + escaped[key] = value.map(v => { + if (isPureObject(v)) { + return escape(v, options, { ...escapeOptions, + escapeKeys: true + }); + } + + return doEscape(v); + }); + } else if (isPureObject(value)) { + escaped[key] = escape(value, options, { ...escapeOptions, + escapeKeys: true }); - } else if (isObject(value)) { - escaped[key] = escape(value, options, escapeOptions); } else { escaped[key] = value; } + + if (escapeKeys) { + const escapedKey = doEscape(key); + + if (key !== escapedKey) { + escaped[escapedKey] = escaped[key]; + delete escaped[key]; + } + } } - return escaped + return escaped; } var isMergeableObject = function isMergeableObject(value) { - return isNonNullObject(value) - && !isSpecial(value) + return isNonNullObject(value) && !isSpecial(value); }; function isNonNullObject(value) { - return !!value && typeof value === 'object' + return !!value && typeof value === 'object'; } function isSpecial(value) { - var stringValue = Object.prototype.toString.call(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 - 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 + return value.$$typeof === REACT_ELEMENT_TYPE; } function emptyTarget(val) { - return Array.isArray(val) ? [] : {} + return Array.isArray(val) ? [] : {}; } function cloneUnlessOtherwiseSpecified(value, options) { - return (options.clone !== false && options.isMergeableObject(value)) - ? deepmerge(emptyTarget(value), value, options) - : value + 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) - }) + 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 + if (!options.customMerge) { + return deepmerge; + } + + var customMerge = options.customMerge(key); + return typeof customMerge === 'function' ? customMerge : deepmerge; } function getEnumerableOwnPropertySymbols(target) { - return Object.getOwnPropertySymbols - ? Object.getOwnPropertySymbols(target).filter(function(symbol) { - return target.propertyIsEnumerable(symbol) - }) - : [] + return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function (symbol) { + return target.propertyIsEnumerable(symbol); + }) : []; } function getKeys(target) { - return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) + return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)); } function mergeObject(target, source, options) { - var destination = {}; - if (options.isMergeableObject(target)) { - getKeys(target).forEach(function(key) { - destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); - }); - } - getKeys(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 + var destination = {}; + + if (options.isMergeableObject(target)) { + getKeys(target).forEach(function (key) { + destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); + }); + } + + getKeys(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; + 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; - 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) - } + 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') - } + if (!Array.isArray(array)) { + throw new Error('first argument should be an array'); + } - return array.reduce(function(prev, next) { - return deepmerge(prev, next, options) - }, {}) + return array.reduce(function (prev, next) { + return deepmerge(prev, next, options); + }, {}); }; var deepmerge_1 = deepmerge; - var cjs = deepmerge_1; - function arrayMerge (ref, target, source) { - var component = ref.component; - var tagIDKeyName = ref.tagIDKeyName; - var metaTemplateKeyName = ref.metaTemplateKeyName; - var contentKeyName = ref.contentKeyName; - + 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 - var destination = []; - - target.forEach(function (targetItem, targetIndex) { + const destination = []; + target.forEach((targetItem, targetIndex) => { // no tagID so no need to check for duplicity if (!targetItem[tagIDKeyName]) { destination.push(targetItem); - return + return; } - var sourceIndex = findIndex(source, function (item) { return item[tagIDKeyName] === targetItem[tagIDKeyName]; }); - var sourceItem = source[sourceIndex]; + const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName]); + const sourceItem = source[sourceIndex]; // source doesnt contain any duplicate vmid's, we can keep targetItem - // 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 + 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 + + + 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 - + 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 - } + return; + } // now we only need to check if the target has a template to combine it with the source + + + const targetTemplate = targetItem[metaTemplateKeyName]; - // 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 + return; } - var sourceTemplate = sourceItem[metaTemplateKeyName]; + const sourceTemplate = sourceItem[metaTemplateKeyName]; if (!sourceTemplate) { // use parent template and child content - applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, targetTemplate); + applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, sourceItem, targetTemplate); } else if (!sourceItem[contentKeyName]) { // use child template and parent content - applyTemplate({ component: component, metaTemplateKeyName: metaTemplateKeyName, contentKeyName: contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]); + applyTemplate({ + component, + metaTemplateKeyName, + contentKeyName + }, sourceItem, undefined, targetItem[contentKeyName]); } }); - - return destination.concat(source) + return destination.concat(source); } - - function merge (target, source, options) { - if ( options === void 0 ) options = {}; - + 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) @@ -772,25 +710,24 @@ delete source.title; } - metaInfoAttributeKeys.forEach(function (attrKey) { + metaInfoAttributeKeys.forEach(attrKey => { if (!source[attrKey]) { - return + return; } - for (var key in source[attrKey]) { + for (const key in source[attrKey]) { if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { - if (booleanHtmlAttributes.includes(key)) { - // eslint-disable-next-line no-console - console.warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); + if (includes(booleanHtmlAttributes, key)) { + warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details'); } + delete source[attrKey][key]; } } }); - return cjs(target, source, { - arrayMerge: function (t, s) { return arrayMerge(options, t, s); } - }) + arrayMerge: (t, s) => arrayMerge(options, t, s) + }); } /** @@ -807,45 +744,46 @@ * @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; + function getComponentOption(options = {}, component, result = {}) { + const { + keyName, + metaTemplateKeyName, + tagIDKeyName + } = options; + const { + $options, + $children + } = component; if (component._inactive) { - return result - } + return result; + } // only collect option data if it exists + - // only collect option data if it exists if ($options[keyName]) { - var data = $options[keyName]; + let data = $options[keyName]; // if option is a function, replace it with it's result - // 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 + - // ignore data if its not an object, then we keep our previous result if (!isObject(data)) { - return result - } + return result; + } // merge with existing options + - // merge with existing options result = merge(result, data, options); - } + } // collect & aggregate child options if deep = true + - // collect & aggregate child options if deep = true if ($children.length) { - $children.forEach(function (childComponent) { + $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 + return; } result = getComponentOption(options, childComponent, result); @@ -854,20 +792,17 @@ if (metaTemplateKeyName && result.meta) { // apply templates if needed - result.meta.forEach(function (metaObject) { return applyTemplate(options, metaObject); }); + result.meta.forEach(metaObject => applyTemplate(options, metaObject)); // remove meta items with duplicate vmid's - // 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]; }) - ) + 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 + return result; } /** @@ -877,56 +812,180 @@ * @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 = []; + function getMetaInfo(options = {}, component, escapeSequences = []) { // collect & aggregate all metaInfo $options - var info = getComponentOption(options, component, defaultInfo); - - // Remove all "template" tags from meta - + 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 + - // 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 + 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] : []; } - 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); } + const escapeOptions = { + doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value) }; - - disableOptionKeys.forEach(function (disableKey, index) { + disableOptionKeys.forEach((disableKey, index) => { if (index === 0) { ensureIsArray(info, disableKey); } else if (index === 1) { - for (var key in info[disableKey]) { + for (const key in info[disableKey]) { ensureIsArray(info[disableKey], key); } } escapeOptions[disableKey] = info[disableKey]; - }); + }); // begin sanitization - // begin sanitization info = escape(info, options, escapeOptions); + return info; + } - return info + function getTag(tags, tag) { + if (!tags[tag]) { + tags[tag] = document.getElementsByTagName(tag)[0]; + } + + return tags[tag]; + } + function getElementsKey({ + body, + pbody + }) { + return body ? 'body' : pbody ? 'pbody' : 'head'; + } + function queryElements(parentNode, { + appId, + attribute, + type, + tagIDKeyName + }, attributes = {}) { + const queries = [`${type}[${attribute}="${appId}"]`, `${type}[data-${tagIDKeyName}]`].map(query => { + for (const key in attributes) { + const val = attributes[key]; + const attributeValue = val && val !== true ? `="${val}"` : ''; + query += `[data-${key}${attributeValue}]`; + } + + return query; + }); + return toArray(parentNode.querySelectorAll(queries.join(', '))); + } + + const callbacks = []; + function isDOMComplete(d = document) { + return d.readyState === 'complete'; + } + function addCallback(query, callback) { + if (arguments.length === 1) { + callback = query; + query = ''; + } + + callbacks.push([query, callback]); + } + function addCallbacks({ + tagIDKeyName + }, type, tags, autoAddListeners) { + let hasAsyncCallback = false; + + for (const tag of tags) { + if (!tag[tagIDKeyName] || !tag.callback) { + continue; + } + + hasAsyncCallback = true; + addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback); + } + + if (!autoAddListeners || !hasAsyncCallback) { + return hasAsyncCallback; + } + + return addListeners(); + } + function addListeners() { + if (isDOMComplete()) { + applyCallbacks(); + return; + } // Instead of using a MutationObserver, we just apply + + /* istanbul ignore next */ + + + document.onreadystatechange = () => { + applyCallbacks(); + }; + } + function applyCallbacks(matchElement) { + for (const [query, callback] of callbacks) { + const selector = `${query}[onload="this.__vm_l=1"]`; + let elements = []; + + if (!matchElement) { + elements = toArray(document.querySelectorAll(selector)); + } + + if (matchElement && matchElement.matches(selector)) { + elements = [matchElement]; + } + + for (const element of elements) { + /* __vm_cb: whether the load callback has been called + * __vm_l: set by onload attribute, whether the element was loaded + * __vm_ev: whether the event listener was added or not + */ + if (element.__vm_cb) { + continue; + } + + const onload = () => { + /* Mark that the callback for this element has already been called, + * this prevents the callback to run twice in some (rare) conditions + */ + element.__vm_cb = true; + /* onload needs to be removed because we only need the + * attribute after ssr and if we dont remove it the node + * will fail isEqualNode on the client + */ + + element.removeAttribute('onload'); + callback(element); + }; + /* IE9 doesnt seem to load scripts synchronously, + * causing a script sometimes/often already to be loaded + * when we add the event listener below (thus adding an onload event + * listener has no use because it will never be triggered). + * Therefore we add the onload attribute during ssr, and + * check here if it was already loaded or not + */ + + + if (element.__vm_l) { + onload(); + continue; + } + + if (!element.__vm_ev) { + element.__vm_ev = true; + element.addEventListener('load', onload); + } + } + } } /** @@ -935,43 +994,38 @@ * @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); + function updateAttribute({ + attribute + } = {}, attrs, tag) { + const vueMetaAttrString = tag.getAttribute(attribute); + const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []; + const toRemove = toArray(vueMetaAttrs); + const keepIndexes = []; - var keepIndexes = []; - for (var attr in attrs) { + for (const attr in attrs) { if (attrs.hasOwnProperty(attr)) { - var value = includes(booleanHtmlAttributes, attr) - ? '' - : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[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 + - // 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); + 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(',')); + tag.setAttribute(attribute, vueMetaAttrs.sort().join(',')); } } @@ -980,9 +1034,9 @@ * * @param {String} title - the new title of the document */ - function updateTitle (title) { - if (title === undefined) { - return + function updateTitle(title) { + if (!title && title !== '') { + return; } document.title = title; @@ -996,98 +1050,143 @@ * @param {(Array<Object>|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 = []; + function updateTag(appId, options = {}, type, tags, head, body) { + const { + attribute, + tagIDKeyName + } = options; + const dataAttributes = [tagIDKeyName, ...commonDataAttributes]; + const newElements = []; + const queryOptions = { + appId, + attribute, + type, + tagIDKeyName + }; + const currentElements = { + head: queryElements(head, queryOptions), + pbody: queryElements(body, queryOptions, { + pbody: true + }), + body: queryElements(body, queryOptions, { + body: true + }) + }; 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); + const found = []; + tags = tags.filter(x => { + const k = JSON.stringify(x); + const res = !includes(found, k); found.push(k); - return res + 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 isBooleanAttribute = includes(booleanHtmlAttributes, attr); - if (isBooleanAttribute && !tag[attr]) { - continue - } - - var value = isBooleanAttribute ? '' : tag[attr]; - newElement.setAttribute(_attr, value); - } - } + for (const tag of tags) { + if (tag.skip) { + continue; } - // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. - var indexToDelete; - var hasEqualElement = oldTags.some(function (existingTag, index) { + const newElement = document.createElement(type); + newElement.setAttribute(attribute, appId); + + for (const attr in tag) { + /* istanbul ignore next */ + if (!tag.hasOwnProperty(attr)) { + continue; + } + + if (attr === 'innerHTML') { + newElement.innerHTML = tag.innerHTML; + continue; + } + + if (attr === 'json') { + newElement.innerHTML = JSON.stringify(tag.json); + continue; + } + + if (attr === 'cssText') { + if (newElement.styleSheet) { + /* istanbul ignore next */ + newElement.styleSheet.cssText = tag.cssText; + } else { + newElement.appendChild(document.createTextNode(tag.cssText)); + } + + continue; + } + + if (attr === 'callback') { + newElement.onload = () => tag[attr](newElement); + + continue; + } + + const _attr = includes(dataAttributes, attr) ? `data-${attr}` : attr; + + const isBooleanAttribute = includes(booleanHtmlAttributes, attr); + + if (isBooleanAttribute && !tag[attr]) { + continue; + } + + const value = isBooleanAttribute ? '' : tag[attr]; + newElement.setAttribute(_attr, value); + } + + const oldElements = currentElements[getElementsKey(tag)]; // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + + let indexToDelete; + const hasEqualElement = oldElements.some((existingTag, index) => { indexToDelete = index; - return newElement.isEqualNode(existingTag) + return newElement.isEqualNode(existingTag); }); if (hasEqualElement && (indexToDelete || indexToDelete === 0)) { - oldTags.splice(indexToDelete, 1); + oldElements.splice(indexToDelete, 1); } else { - newTags.push(newElement); + newElements.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] + let oldElements = []; + + for (const current of Object.values(currentElements)) { + oldElements = [...oldElements, ...current]; + } // remove old elements + + + for (const element of oldElements) { + element.parentNode.removeChild(element); + } // insert new elements + + + for (const element of newElements) { + if (element.hasAttribute('data-body')) { + body.appendChild(element); + continue; + } + + if (element.hasAttribute('data-pbody')) { + body.insertBefore(element, body.firstChild); + continue; + } + + head.appendChild(element); + } + + return { + oldTags: oldElements, + newTags: newElements + }; } /** @@ -1095,60 +1194,66 @@ * * @param {Object} newInfo - the meta info to update to */ - function updateClientMetaInfo (appId, options, newInfo) { - if ( options === void 0 ) options = {}; - var ssrAttribute = options.ssrAttribute; + function updateClientMetaInfo(appId, options = {}, newInfo) { + const { + ssrAttribute, + ssrAppId + } = options; // only cache tags for current update - // only cache tags for current update - var tags = {}; + const tags = {}; + const htmlTag = getTag(tags, 'html'); // if this is a server render, then dont update - var htmlTag = getTag(tags, 'html'); - - // if this is a server render, then dont update - if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) { + if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) { // remove the server render attribute so we can update on (next) changes - htmlTag.removeAttribute(ssrAttribute); - return false - } + htmlTag.removeAttribute(ssrAttribute); // add load callbacks if the - // initialize tracked changes - var addedTags = {}; - var removedTags = {}; + let addLoadListeners = false; - for (var type in newInfo) { + for (const type of tagsSupportingOnload) { + if (newInfo[type] && addCallbacks(options, type, newInfo[type])) { + addLoadListeners = true; + } + } + + if (addLoadListeners) { + addListeners(); + } + + return false; + } // initialize tracked changes + + + const addedTags = {}; + const removedTags = {}; + + for (const type in newInfo) { // ignore these if (includes(metaInfoOptionKeys, type)) { - continue + continue; } if (type === 'title') { // update the title updateTitle(newInfo.title); - continue + continue; } if (includes(metaInfoAttributeKeys, type)) { - var tagName = type.substr(0, 4); + const tagName = type.substr(0, 4); updateAttribute(options, newInfo[type], getTag(tags, tagName)); - continue - } + continue; + } // tags should always be an array, ignore if it isnt + - // tags should always be an array, ignore if it isnt if (!isArray(newInfo[type])) { - continue + continue; } - var ref = updateTag( - appId, - options, - type, - newInfo[type], - getTag(tags, 'head'), - getTag(tags, 'body') - ); - var oldTags = ref.oldTags; - var newTags = ref.newTags; + const { + oldTags, + newTags + } = updateTag(appId, options, type, newInfo[type], getTag(tags, 'head'), getTag(tags, 'body')); if (newTags.length) { addedTags[type] = newTags; @@ -1156,12 +1261,13 @@ } } - return { addedTags: addedTags, removedTags: removedTags } + return { + addedTags, + removedTags + }; } - function _refresh (options) { - if ( options === void 0 ) options = {}; - + 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 @@ -1172,32 +1278,35 @@ * * @return {Object} - new meta info */ - return function refresh () { - var metaInfo = getMetaInfo(options, this.$root, clientSequences); + 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 - 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 } - } + return { + vm: this, + metaInfo, + tags + }; + }; } - function _$meta (options) { - if ( options === void 0 ) options = {}; - - var _refresh$1 = _refresh(options); - var inject = function () {}; + 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 () { + + + return function $meta() { if (!this.$root._vueMeta) { return { getOptions: showWarningNotSupported, @@ -1205,48 +1314,45 @@ inject: showWarningNotSupported, pause: showWarningNotSupported, resume: showWarningNotSupported - } + }; } return { - getOptions: function () { return getOptions(options); }, + getOptions: () => getOptions(options), refresh: _refresh$1.bind(this), - inject: 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 = {}; + function install(Vue, options = {}) { if (Vue.__vuemeta_installed) { - return + return; } + Vue.__vuemeta_installed = true; - options = setOptions(options); - Vue.prototype.$meta = _$meta(options); - Vue.mixin(createMixin(Vue, options)); - } + } // automatic install + - // automatic install if (!isUndefined(window) && !isUndefined(window.Vue)) { /* istanbul ignore next */ install(window.Vue); } var browser = { - version: version, - install: install, - hasMetaInfo: hasMetaInfo + version, + install, + hasMetaInfo }; return browser; diff --git a/dist/vue-meta.min.js b/dist/vue-meta.min.js index 9d2cd25..6b51206 100644 --- a/dist/vue-meta.min.js +++ b/dist/vue-meta.min.js @@ -1 +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 c(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 f=1;var l={title:void 0,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<e.length;r++)if(t.call(n[2],e[r],r,e))return r;return-1}return e.findIndex(t,arguments[2])}function _(e){return Array.from?Array.from(e):Array.prototype.slice.call(e)}function T(e,t){if(!Array.prototype.includes){for(var n in e)if(e[n]===t)return!0;return!1}return e.includes(t)}var A=[[/&/g,"&"],[/</g,"<"],[/>/g,">"],[/"/g,'"'],[/'/g,"'"]];var N=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===w}(e)}(e)};var w="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function I(e,t){return!1!==t.clone&&t.isMergeableObject(e)?j((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function O(e,t,n){return e.concat(t).map(function(e){return I(e,n)})}function S(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter(function(t){return e.propertyIsEnumerable(t)}):[]}(e))}function E(e,t,n){var r={};return n.isMergeableObject(e)&&S(e).forEach(function(t){r[t]=I(e[t],n)}),S(t).forEach(function(i){n.isMergeableObject(t[i])&&e[i]?r[i]=function(e,t){if(!t.customMerge)return j;var n=t.customMerge(e);return"function"==typeof n?n:j}(i,n)(e[i],t[i],n):r[i]=I(t[i],n)}),r}function j(e,t,n){(n=n||{}).arrayMerge=n.arrayMerge||O,n.isMergeableObject=n.isMergeableObject||N;var r=Array.isArray(t);return r===Array.isArray(e)?r?n.arrayMerge(e,t,n):E(e,t,n):I(t,n)}j.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce(function(e,n){return j(e,n,t)},{})};var z=j;function D(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]&&(m.includes(n)&&console.warn("VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details"),delete t[e][n])}),z(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]}),c=n[s];if(-1!==s){if(c.hasOwnProperty(a)&&void 0===c[a]||c.hasOwnProperty("innerHTML")&&void 0===c.innerHTML)return u.push(e),void n.splice(s,1);if(null!==c[a]&&null!==c.innerHTML){var f=e[o];f&&(c[o]?c[a]||$({component:r,metaTemplateKeyName:o,contentKeyName:a},c,void 0,e[a]):$({component:r,metaTemplateKeyName:o,contentKeyName:a},c,f))}else n.splice(s,1)}else u.push(e)}else u.push(e)}),u.concat(n)}(n,e,t)}})}function k(e,t,n){void 0===e&&(e={}),void 0===n&&(n={});var a=e.keyName,u=e.metaTemplateKeyName,s=e.tagIDKeyName,c=t.$options,f=t.$children;if(t._inactive)return n;if(c[a]){var l=c[a];if(o(l)&&(l=l.call(t)),!i(l))return n;n=D(n,l,e)}return f.length&&f.forEach(function(t){(function(e){return void 0===e&&(e=this),e&&!r(e._vueMeta)})(t)&&(n=k(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 K(e,t,r){void 0===e&&(e={}),void 0===r&&(r=[]);var o=k(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 c in t){var f=t[c];if(T(v,c))s[c]=f;else{var l=h[0];if(o[l]&&T(o[l],c))s[c]=f;else{var d=t[a];d&&(l=h[1],o[l]&&o[l][d]&&T(o[l][d],c))?s[c]=f:"string"==typeof f?s[c]=u(f):n(f)?s[c]=f.map(function(t){return i(t)?e(t,r,o):u(t)}):i(f)?s[c]=e(f,r,o):s[c]=f}}}return s}(o,e,u)}function x(e,t,r){void 0===e&&(e={});var i=e.attribute,o=r.getAttribute(i),a=o?o.split(","):[],u=_(a),s=[];for(var c in t)if(t.hasOwnProperty(c)){var f=T(m,c)?"":n(t[c])?t[c].join(" "):t[c];r.setAttribute(c,f||""),T(a,c)||a.push(c),s.push(u.indexOf(c))}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 P(e,t,n,r,i,o){void 0===t&&(t={});var a=t.attribute,u=t.tagIDKeyName,s=_(i.querySelectorAll(n+"["+a+'="'+e+'"], '+n+"[data-"+u+"]")),c=_(o.querySelectorAll(n+"["+a+'="'+e+'"][data-body="true"], '+n+"[data-"+u+'][data-body="true"]')),f=[u,"body"],l=[];if(r.length>1){var d=[];r=r.filter(function(e){var t=JSON.stringify(e),n=!T(d,t);return d.push(t),n})}r.length&&r.forEach(function(t){var r=document.createElement(n);r.setAttribute(a,e);var i,o=!0!==t.body?s:c;for(var u in t)if(t.hasOwnProperty(u))if("innerHTML"===u)r.innerHTML=t.innerHTML;else if("cssText"===u)r.styleSheet?r.styleSheet.cssText=t.cssText:r.appendChild(document.createTextNode(t.cssText));else{var d=T(f,u)?"data-"+u:u,v=T(m,u);if(v&&!t[u])continue;var h=v?"":t[u];r.setAttribute(d,h)}o.some(function(e,t){return i=t,r.isEqualNode(e)})&&(i||0===i)?o.splice(i,1):l.push(r)});var v=s.concat(c);return v.forEach(function(e){return e.parentNode.removeChild(e)}),l.forEach(function(e){"true"===e.getAttribute("data-body")?o.appendChild(e):i.appendChild(e)}),{oldTags:v,newTags:l}}function C(e,t){return e[t]||(e[t]=document.getElementsByTagName(t)[0]),e[t]}function H(e){return void 0===e&&(e={}),function(){var t=K(e,this.$root,A),r=function(e,t,r){void 0===t&&(t={});var i=t.ssrAttribute,o={},a=C(o,"html");if("ssr"===e&&a.hasAttribute(i))return a.removeAttribute(i),!1;var u,s={},c={};for(var f in r)if(!T(v,f))if("title"!==f){if(T(p,f)){var l=f.substr(0,4);x(t,r[f],C(o,l))}else if(n(r[f])){var d=P(e,t,f,r[f],C(o,"head"),C(o,"body")),h=d.oldTags,m=d.newTags;m.length&&(s[f]=m,c[f]=h)}}else void 0!==(u=r.title)&&(document.title=u);return{addedTags:s,removedTags:c}}(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 L(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=H(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:f},f++),!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&&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&&c(this)}))}),n.refreshOnceOnNavigation&&c(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)||L(window.Vue),{version:"2.0.5",install:L,hasMetaInfo:s}}); +!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";let 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=10){clearTimeout(e),e=setTimeout(()=>{t()},n)}(()=>t.$meta().refresh())}function n(e){return Array.isArray(e)}function o(e){return void 0===e}function r(e){return"object"==typeof e}function i(e){return"object"==typeof e&&null!==e}function a(e){return"function"==typeof e}function s(e,t){return t&&r(e)?(n(e[t])||(e[t]=[]),e):n(e)?e:[]}function u(e,t,n){s(e,t),e[t].push(n)}function c(e=this){return e&&(!0===e._vueMeta||r(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,o)=>{n.pause(),o()}),t.afterEach(()=>{const{metaInfo:e}=n.resume();e&&e.afterNavigation&&a(e.afterNavigation)&&e.afterNavigation(e)})}const f=function(){try{return!o(window)}catch(e){return!1}}()?window:global,d=f.console=f.console||{};function h(...e){d&&d.warn&&d.warn(...e)}const p=()=>h("This vue app/component has no vue-meta configuration");let m=1;const y={title:void 0,titleChunk:"",titleTemplate:"%s",htmlAttrs:{},bodyAttrs:{},headAttrs:{},base:[],link:[],meta:[],style:[],script:[],noscript:[],__dangerouslyDisableSanitizers:[],__dangerouslyDisableSanitizersByTagID:{}},v={keyName:"metaInfo",attribute:"data-vue-meta",ssrAttribute:"data-vue-meta-server-rendered",tagIDKeyName:"vmid",contentKeyName:"content",metaTemplateKeyName:"template",ssrAppId:"ssr"},b=["titleChunk","titleTemplate","changed","__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],g=["__dangerouslyDisableSanitizers","__dangerouslyDisableSanitizersByTagID"],$=["htmlAttrs","headAttrs","bodyAttrs"],_=["link","style","script"],M=["body","pbody"],A=["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 T(e=!0){return this.$root._vueMeta.paused=!0,()=>N(e)}function N(e=!0){if(this.$root._vueMeta.paused=!1,e)return this.$root.$meta().refresh()}function I({component:e,metaTemplateKeyName:t,contentKeyName:n},r,i,s){return o(i)&&(i=r[t],delete r[t]),!!i&&(o(s)&&(s=r[n]),r[n]=a(i)?i.call(e,s):i.replace(/%s/g,s),!0)}function w(e,t){if(!Array.prototype.findIndex){for(let n=0;n<e.length;n++)if(t.call(arguments[2],e[n],n,e))return n;return-1}return e.findIndex(t,arguments[2])}function O(e){return Array.from?Array.from(e):Array.prototype.slice.call(e)}function j(e,t){if(!Array.prototype.includes){for(const n in e)if(e[n]===t)return!0;return!1}return e.includes(t)}const S=[[/&/g,"&"],[/</g,"<"],[/>/g,">"],[/"/g,'"'],[/'/g,"'"]];var K=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===k}(e)}(e)};var k="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function D(e,t){return!1!==t.clone&&t.isMergeableObject(e)?P((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function z(e,t,n){return e.concat(t).map(function(e){return D(e,n)})}function E(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter(function(t){return e.propertyIsEnumerable(t)}):[]}(e))}function x(e,t,n){var o={};return n.isMergeableObject(e)&&E(e).forEach(function(t){o[t]=D(e[t],n)}),E(t).forEach(function(r){n.isMergeableObject(t[r])&&e[r]?o[r]=function(e,t){if(!t.customMerge)return P;var n=t.customMerge(e);return"function"==typeof n?n:P}(r,n)(e[r],t[r],n):o[r]=D(t[r],n)}),o}function P(e,t,n){(n=n||{}).arrayMerge=n.arrayMerge||z,n.isMergeableObject=n.isMergeableObject||K;var o=Array.isArray(t);return o===Array.isArray(e)?o?n.arrayMerge(e,t,n):x(e,t,n):D(t,n)}P.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce(function(e,n){return P(e,n,t)},{})};var C=P;function L(e,t,n={}){return t.hasOwnProperty("title")&&void 0===t.title&&delete t.title,$.forEach(e=>{if(t[e])for(const n in t[e])t[e].hasOwnProperty(n)&&void 0===t[e][n]&&(j(A,n)&&h("VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details"),delete t[e][n])}),C(e,t,{arrayMerge:(e,t)=>(function({component:e,tagIDKeyName:t,metaTemplateKeyName:n,contentKeyName:o},r,i){const a=[];return r.forEach((r,s)=>{if(!r[t])return void a.push(r);const u=w(i,e=>e[t]===r[t]),c=i[u];if(-1===u)return void a.push(r);if(c.hasOwnProperty(o)&&void 0===c[o]||c.hasOwnProperty("innerHTML")&&void 0===c.innerHTML)return a.push(r),void i.splice(u,1);if(null===c[o]||null===c.innerHTML)return void i.splice(u,1);const l=r[n];l&&(c[n]?c[o]||I({component:e,metaTemplateKeyName:n,contentKeyName:o},c,void 0,r[o]):I({component:e,metaTemplateKeyName:n,contentKeyName:o},c,l))}),a.concat(i)})(n,e,t)})}function H(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(a(o)&&(o=o.call(t)),!r(o))return n;n=L(n,o,e)}return l.length&&l.forEach(t=>{(function(e=this){return e&&!o(e._vueMeta)})(t)&&(n=H(e,t,n))}),s&&n.meta&&(n.meta.forEach(t=>I(e,t)),n.meta=n.meta.filter((e,t,n)=>!e.hasOwnProperty(u)||t===w(n,t=>t[u]===e[u]))),n}function B(e={},t,o=[]){let r=H(e,t,y);r.title&&(r.titleChunk=r.title),r.titleTemplate&&"%s"!==r.titleTemplate&&I({component:t,contentKeyName:"title"},r,r.titleTemplate,r.titleChunk||""),r.base&&(r.base=Object.keys(r.base).length?[r.base]:[]);const a={doEscape:e=>o.reduce((e,[t,n])=>e.replace(t,n),e)};return g.forEach((e,t)=>{if(0===t)s(r,e);else if(1===t)for(const t in r[e])s(r[e],t);a[e]=r[e]}),r=function e(t,o,r){const{tagIDKeyName:a}=o,{doEscape:s=(e=>e),escapeKeys:u}=r,c={};for(const l in t){const f=t[l];if(j(b,l)){c[l]=f;continue}let[d]=g;if(r[d]&&j(r[d],l)){c[l]=f;continue}const h=t[a];if(h&&(d=g[1],r[d]&&r[d][h]&&j(r[d][h],l)))c[l]=f;else if("string"==typeof f?c[l]=s(f):n(f)?c[l]=f.map(t=>i(t)?e(t,o,{...r,escapeKeys:!0}):s(t)):i(f)?c[l]=e(f,o,{...r,escapeKeys:!0}):c[l]=f,u){const e=s(l);l!==e&&(c[e]=c[l],delete c[l])}}return c}(r,e,a)}function V(e,t){return e[t]||(e[t]=document.getElementsByTagName(t)[0]),e[t]}function q({body:e,pbody:t}){return e?"body":t?"pbody":"head"}function W(e,{appId:t,attribute:n,type:o,tagIDKeyName:r},i={}){const a=[`${o}[${n}="${t}"]`,`${o}[data-${r}]`].map(e=>{for(const t in i){const n=i[t];e+=`[data-${t}${n&&!0!==n?`="${n}"`:""}]`}return e});return O(e.querySelectorAll(a.join(", ")))}const G=[];function J(e,t){1===arguments.length&&(t=e,e=""),G.push([e,t])}function R({tagIDKeyName:e},t,n,o){let r=!1;for(const o of n)o[e]&&o.callback&&(r=!0,J(`${t}[data-${e}="${o[e]}"]`,o.callback));return o&&r?F():r}function F(){!function(e=document){return"complete"===e.readyState}()?document.onreadystatechange=()=>{Q()}:Q()}function Q(e){for(const[t,n]of G){const o=`${t}[onload="this.__vm_l=1"]`;let r=[];e||(r=O(document.querySelectorAll(o))),e&&e.matches(o)&&(r=[e]);for(const e of r){if(e.__vm_cb)continue;const t=()=>{e.__vm_cb=!0,e.removeAttribute("onload"),n(e)};e.__vm_l?t():e.__vm_ev||(e.__vm_ev=!0,e.addEventListener("load",t))}}}function U({attribute:e}={},t,o){const r=o.getAttribute(e),i=r?r.split(","):[],a=O(i),s=[];for(const e in t)if(t.hasOwnProperty(e)){const r=j(A,e)?"":n(t[e])?t[e].join(" "):t[e];o.setAttribute(e,r||""),j(i,e)||i.push(e),s.push(a.indexOf(e))}const u=a.filter((e,t)=>!j(s,t)).reduce((e,t)=>(o.removeAttribute(t),e+1),0);i.length===u?o.removeAttribute(e):o.setAttribute(e,i.sort().join(","))}function X(e,t={},n,o,r,i){const{attribute:a,tagIDKeyName:s}=t,u=[s,...M],c=[],l={appId:e,attribute:a,type:n,tagIDKeyName:s},f={head:W(r,l),pbody:W(i,l,{pbody:!0}),body:W(i,l,{body:!0})};if(o.length>1){const e=[];o=o.filter(t=>{const n=JSON.stringify(t),o=!j(e,n);return e.push(n),o})}if(o.length)for(const t of o){if(t.skip)continue;const o=document.createElement(n);o.setAttribute(a,e);for(const e in t){if(!t.hasOwnProperty(e))continue;if("innerHTML"===e){o.innerHTML=t.innerHTML;continue}if("json"===e){o.innerHTML=JSON.stringify(t.json);continue}if("cssText"===e){o.styleSheet?o.styleSheet.cssText=t.cssText:o.appendChild(document.createTextNode(t.cssText));continue}if("callback"===e){o.onload=()=>t[e](o);continue}const n=j(u,e)?`data-${e}`:e,r=j(A,e);if(r&&!t[e])continue;const i=r?"":t[e];o.setAttribute(n,i)}const r=f[q(t)];let i;r.some((e,t)=>(i=t,o.isEqualNode(e)))&&(i||0===i)?r.splice(i,1):c.push(o)}let d=[];for(const e of Object.values(f))d=[...d,...e];for(const e of d)e.parentNode.removeChild(e);for(const e of c)e.hasAttribute("data-body")?i.appendChild(e):e.hasAttribute("data-pbody")?i.insertBefore(e,i.firstChild):r.appendChild(e);return{oldTags:d,newTags:c}}function Y(e={}){return function(){const t=B(e,this.$root,S),o=function(e,t={},o){const{ssrAttribute:r,ssrAppId:i}=t,a={},s=V(a,"html");if(e===i&&s.hasAttribute(r)){s.removeAttribute(r);let e=!1;for(const n of _)o[n]&&R(t,n,o[n])&&(e=!0);return e&&F(),!1}const u={},c={};for(const r in o){if(j(b,r))continue;if("title"===r){((l=o.title)||""===l)&&(document.title=l);continue}if(j($,r)){const e=r.substr(0,4);U(t,o[r],V(a,e));continue}if(!n(o[r]))continue;const{oldTags:i,newTags:s}=X(e,t,r,o[r],V(a,"head"),V(a,"body"));s.length&&(u[r]=s,c[r]=i)}var l;return{addedTags:u,removedTags:c}}(this.$root._vueMeta.appId,e,t);return o&&a(t.changed)&&t.changed(t,o.addedTags,o.removedTags),{vm:this,metaInfo:t,tags:o}}}function Z(e,n={}){e.__vuemeta_installed||(e.__vuemeta_installed=!0,n=function(e){e=r(e)?e:{};for(const t in v)e[t]||(e[t]=v[t]);return e}(n),e.prototype.$meta=function(e={}){const t=Y(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:T.bind(this),resume:N.bind(this)}:{getOptions:p,refresh:p,inject:p,pause:p,resume:p}}}(n),e.mixin(function(e,n){const r=["activated","deactivated","beforeMount"];return{beforeCreate(){if(Object.defineProperty(this,"_hasMetaInfo",{configurable:!0,get(){return e.config.devtools&&!this.$root._vueMeta.hasMetaInfoDeprecationWarningShown&&(h("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[n.keyName])&&null!==this.$options[n.keyName]){if(this.$root._vueMeta||(this.$root._vueMeta={appId:m},m++),!this._vueMeta){this._vueMeta=!0;let e=this.$parent;for(;e&&e!==this.$root;)o(e._vueMeta)&&(e._vueMeta=!1),e=e.$parent}a(this.$options[n.keyName])&&(this.$options.computed||(this.$options.computed={}),this.$options.computed.$metaInfo=this.$options[n.keyName],this.$isServer||u(this.$options,"created",()=>{this.$watch("$metaInfo",function(){t(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&&this.$root.$el.hasAttribute("data-server-rendered")&&(this.$root._vueMeta.appId=n.ssrAppId)}),u(this.$options,"mounted",()=>{this.$root._vueMeta.initialized||(this.$root._vueMeta.initializing=!0,this.$nextTick(function(){const{tags:e,metaInfo:o}=this.$root.$meta().refresh();!1===e&&null===this.$root._vueMeta.initialized&&this.$nextTick(()=>t(this,"initializing")),this.$root._vueMeta.initialized=!0,delete this.$root._vueMeta.initializing,!n.refreshOnceOnNavigation&&o.afterNavigation&&l(this)}))}),n.refreshOnceOnNavigation&&l(this))),this.$isServer||(r.forEach(e=>{u(this.$options,e,()=>t(this,e))}),u(this.$options,"destroyed",()=>{const e=setInterval(()=>{this.$el&&null!==this.$el.offsetParent||(clearInterval(e),this.$parent&&t(this,"destroyed"))},50)}))}}}}(e,n)))}return o(window)||o(window.Vue)||Z(window.Vue),{version:"2.1.0",install:Z,hasMetaInfo:c}}); diff --git a/package.json b/package.json index 92ac04f..403c43d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-meta", - "version": "2.0.5", + "version": "2.1.0", "description": "Manage HTML metadata in Vue.js components with ssr support", "keywords": [ "attribute",