mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-16 16:10:33 +03:00
feat: major refactor, cleanup and jest tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// fallback to timers if rAF not present
|
||||
const stopUpdate = (typeof window !== 'undefined' ? window.cancelAnimationFrame : null) || clearTimeout
|
||||
const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFrame : null) || ((cb) => setTimeout(cb, 0))
|
||||
const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
|
||||
|
||||
/**
|
||||
* Performs a batched update. Uses requestAnimationFrame to prevent
|
||||
@@ -12,7 +12,7 @@ const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFram
|
||||
* @param {Function} callback - the update to perform
|
||||
* @return {Number} id - a new ID
|
||||
*/
|
||||
export default function batchUpdate (id, callback) {
|
||||
export default function batchUpdate(id, callback) {
|
||||
stopUpdate(id)
|
||||
return startUpdate(() => {
|
||||
id = null
|
||||
|
||||
+12
-5
@@ -1,7 +1,7 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||
|
||||
export default function _refresh (options = {}) {
|
||||
export default 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
|
||||
@@ -12,9 +12,16 @@ export default function _refresh (options = {}) {
|
||||
*
|
||||
* @return {Object} - new meta info
|
||||
*/
|
||||
return function refresh () {
|
||||
const info = getMetaInfo(options)(this.$root)
|
||||
updateClientMetaInfo(options).call(this, info)
|
||||
return info
|
||||
return function refresh() {
|
||||
const metaInfo = getMetaInfo(options, this.$root)
|
||||
|
||||
const tags = updateClientMetaInfo(options, metaInfo)
|
||||
|
||||
// emit "event" with new info
|
||||
if (tags && typeof metaInfo.changed === 'function') {
|
||||
metaInfo.changed.call(this, metaInfo, tags.addedTags, tags.removedTags)
|
||||
}
|
||||
|
||||
return metaInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,75 @@
|
||||
import updateTitle from './updaters/updateTitle'
|
||||
import updateTagAttributes from './updaters/updateTagAttributes'
|
||||
import updateTags from './updaters/updateTags'
|
||||
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||
|
||||
export default function _updateClientMetaInfo (options = {}) {
|
||||
const getTag = (tags, tag) => {
|
||||
if (!tags[tag]) {
|
||||
tags[tag] = document.getElementsByTagName(tag)[0]
|
||||
}
|
||||
|
||||
return tags[tag]
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @param {Object} newInfo - the meta info to update to
|
||||
*/
|
||||
export default function updateClientMetaInfo(options = {}, newInfo) {
|
||||
const { ssrAttribute } = options
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @param {Object} newInfo - the meta info to update to
|
||||
*/
|
||||
return function updateClientMetaInfo (newInfo) {
|
||||
const htmlTag = document.getElementsByTagName('html')[0]
|
||||
// if this is not a server render, then update
|
||||
if (htmlTag.getAttribute(ssrAttribute) === null) {
|
||||
// initialize tracked changes
|
||||
const addedTags = {}
|
||||
const removedTags = {}
|
||||
// only cache tags for current update
|
||||
const tags = {}
|
||||
|
||||
Object.keys(newInfo).forEach((key) => {
|
||||
switch (key) {
|
||||
// update the title
|
||||
case 'title':
|
||||
updateTitle(options)(newInfo.title)
|
||||
break
|
||||
// update attributes
|
||||
case 'htmlAttrs':
|
||||
updateTagAttributes(options)(newInfo[key], htmlTag)
|
||||
break
|
||||
case 'bodyAttrs':
|
||||
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('body')[0])
|
||||
break
|
||||
case 'headAttrs':
|
||||
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('head')[0])
|
||||
break
|
||||
// ignore these
|
||||
case 'titleChunk':
|
||||
case 'titleTemplate':
|
||||
case 'changed':
|
||||
case '__dangerouslyDisableSanitizers':
|
||||
break
|
||||
// catch-all update tags
|
||||
default:
|
||||
const headTag = document.getElementsByTagName('head')[0]
|
||||
const bodyTag = document.getElementsByTagName('body')[0]
|
||||
const { oldTags, newTags } = updateTags(options)(key, newInfo[key], headTag, bodyTag)
|
||||
if (newTags.length) {
|
||||
addedTags[key] = newTags
|
||||
removedTags[key] = oldTags
|
||||
}
|
||||
}
|
||||
})
|
||||
const htmlTag = getTag(tags, 'html')
|
||||
|
||||
// emit "event" with new info
|
||||
if (typeof newInfo.changed === 'function') {
|
||||
newInfo.changed.call(this, newInfo, addedTags, removedTags)
|
||||
// if this is not a server render, then update
|
||||
if (htmlTag.getAttribute(ssrAttribute) === null) {
|
||||
// initialize tracked changes
|
||||
const addedTags = {}
|
||||
const removedTags = {}
|
||||
|
||||
Object.keys(newInfo).forEach((type) => {
|
||||
// ignore these
|
||||
if ([
|
||||
'titleChunk',
|
||||
'titleTemplate',
|
||||
'changed',
|
||||
'__dangerouslyDisableSanitizers',
|
||||
'__dangerouslyDisableSanitizersByTagID'
|
||||
].includes(type)) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// remove the server render attribute so we can update on changes
|
||||
htmlTag.removeAttribute(ssrAttribute)
|
||||
}
|
||||
|
||||
if (type === 'title') {
|
||||
// update the title
|
||||
updateTitle(newInfo.title)
|
||||
return
|
||||
}
|
||||
|
||||
if (['htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||
const tagName = type.substr(0, 4)
|
||||
updateAttribute(options, newInfo[type], getTag(tags, tagName))
|
||||
return
|
||||
}
|
||||
|
||||
const { oldTags, newTags } = updateTag(
|
||||
options,
|
||||
type,
|
||||
newInfo[type],
|
||||
getTag(tags, 'head'),
|
||||
getTag(tags, 'body')
|
||||
)
|
||||
|
||||
if (newTags.length) {
|
||||
addedTags[type] = newTags
|
||||
removedTags[type] = oldTags
|
||||
}
|
||||
})
|
||||
|
||||
return { addedTags, removedTags }
|
||||
} else {
|
||||
// remove the server render attribute so we can update on changes
|
||||
htmlTag.removeAttribute(ssrAttribute)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Updates the document's html tag attributes
|
||||
*
|
||||
* @param {Object} attrs - the new document html attributes
|
||||
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
||||
*/
|
||||
export default function updateAttribute({ attribute } = {}, attrs, tag) {
|
||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
||||
const toRemove = [].concat(vueMetaAttrs)
|
||||
|
||||
for (const attr in attrs) {
|
||||
if (attrs.hasOwnProperty(attr)) {
|
||||
const val = attrs[attr] || ''
|
||||
tag.setAttribute(attr, val)
|
||||
|
||||
if (vueMetaAttrs.indexOf(attr) === -1) {
|
||||
vueMetaAttrs.push(attr)
|
||||
}
|
||||
|
||||
const keepIndex = toRemove.indexOf(attr)
|
||||
if (keepIndex !== -1) {
|
||||
toRemove.splice(keepIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toRemove.forEach(attr => tag.removeAttribute(attr))
|
||||
|
||||
if (vueMetaAttrs.length === toRemove.length) {
|
||||
tag.removeAttribute(attribute)
|
||||
} else {
|
||||
tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(','))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as updateAttribute } from './attribute'
|
||||
export { default as updateTitle } from './title'
|
||||
export { default as updateTag } from './tag'
|
||||
@@ -0,0 +1,84 @@
|
||||
// borrow the slice method
|
||||
const toArray = Function.prototype.call.bind(Array.prototype.slice)
|
||||
|
||||
/**
|
||||
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
||||
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - a representation of what tags changed
|
||||
*/
|
||||
export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) {
|
||||
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
|
||||
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
|
||||
const newTags = []
|
||||
|
||||
if (tags.length > 1) {
|
||||
// remove duplicates that could have been found by merging tags
|
||||
// which include a mixin with metaInfo and that mixin is used
|
||||
// by multiple components on the same page
|
||||
const found = []
|
||||
tags = tags.filter((x) => {
|
||||
const k = JSON.stringify(x)
|
||||
const res = !found.includes(k)
|
||||
found.push(k)
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (tags && tags.length) {
|
||||
tags.forEach((tag) => {
|
||||
const newElement = document.createElement(type)
|
||||
newElement.setAttribute(attribute, 'true')
|
||||
|
||||
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) {
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
} else if ([tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
||||
const _attr = `data-${attr}`
|
||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
} else {
|
||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||
newElement.setAttribute(attr, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
||||
let indexToDelete
|
||||
const hasEqualElement = oldTags.some((existingTag, index) => {
|
||||
indexToDelete = index
|
||||
return newElement.isEqualNode(existingTag)
|
||||
})
|
||||
|
||||
if (hasEqualElement && (indexToDelete || indexToDelete === 0)) {
|
||||
oldTags.splice(indexToDelete, 1)
|
||||
} else {
|
||||
newTags.push(newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const oldTags = oldHeadTags.concat(oldBodyTags)
|
||||
oldTags.forEach(tag => tag.parentNode.removeChild(tag))
|
||||
newTags.forEach((tag) => {
|
||||
if (tag.getAttribute('data-body') === 'true') {
|
||||
bodyTag.appendChild(tag)
|
||||
} else {
|
||||
headTag.appendChild(tag)
|
||||
}
|
||||
})
|
||||
|
||||
return { oldTags, newTags }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Updates the document title
|
||||
*
|
||||
* @param {String} title - the new title of the document
|
||||
*/
|
||||
export default function updateTitle(title = document.title) {
|
||||
document.title = title
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export default function _updateTagAttributes (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Updates the document's html tag attributes
|
||||
*
|
||||
* @param {Object} attrs - the new document html attributes
|
||||
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
||||
*/
|
||||
return function updateTagAttributes (attrs, tag) {
|
||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
||||
const toRemove = [].concat(vueMetaAttrs)
|
||||
for (let attr in attrs) {
|
||||
if (attrs.hasOwnProperty(attr)) {
|
||||
const val = attrs[attr] || ''
|
||||
tag.setAttribute(attr, val)
|
||||
if (vueMetaAttrs.indexOf(attr) === -1) {
|
||||
vueMetaAttrs.push(attr)
|
||||
}
|
||||
const saveIndex = toRemove.indexOf(attr)
|
||||
if (saveIndex !== -1) {
|
||||
toRemove.splice(saveIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = toRemove.length - 1
|
||||
for (; i >= 0; i--) {
|
||||
tag.removeAttribute(toRemove[i])
|
||||
}
|
||||
if (vueMetaAttrs.length === toRemove.length) {
|
||||
tag.removeAttribute(attribute)
|
||||
} else {
|
||||
tag.setAttribute(attribute, vueMetaAttrs.join(','))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// borrow the slice method
|
||||
const toArray = Function.prototype.call.bind(Array.prototype.slice)
|
||||
|
||||
export default function _updateTags (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
||||
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - a representation of what tags changed
|
||||
*/
|
||||
return function updateTags (type, tags, headTag, bodyTag) {
|
||||
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
|
||||
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
|
||||
const newTags = []
|
||||
let indexToDelete
|
||||
|
||||
if (tags.length > 1) {
|
||||
// remove duplicates that could have been found by merging tags
|
||||
// which include a mixin with metaInfo and that mixin is used
|
||||
// by multiple components on the same page
|
||||
const found = []
|
||||
tags = tags.map(x => {
|
||||
const k = JSON.stringify(x)
|
||||
if (found.indexOf(k) < 0) {
|
||||
found.push(k)
|
||||
return x
|
||||
}
|
||||
}).filter(x => x)
|
||||
}
|
||||
|
||||
if (tags && tags.length) {
|
||||
tags.forEach((tag) => {
|
||||
const newElement = document.createElement(type)
|
||||
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) {
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
} else if ([options.tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
||||
const _attr = `data-${attr}`
|
||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
} else {
|
||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||
newElement.setAttribute(attr, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newElement.setAttribute(attribute, 'true')
|
||||
|
||||
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
||||
if (oldTags.some((existingTag, index) => {
|
||||
indexToDelete = index
|
||||
return newElement.isEqualNode(existingTag)
|
||||
})) {
|
||||
oldTags.splice(indexToDelete, 1)
|
||||
} else {
|
||||
newTags.push(newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
const oldTags = oldHeadTags.concat(oldBodyTags)
|
||||
oldTags.forEach((tag) => tag.parentNode.removeChild(tag))
|
||||
newTags.forEach((tag) => {
|
||||
if (tag.getAttribute('data-body') === 'true') {
|
||||
bodyTag.appendChild(tag)
|
||||
} else {
|
||||
headTag.appendChild(tag)
|
||||
}
|
||||
})
|
||||
|
||||
return { oldTags, newTags }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function _updateTitle () {
|
||||
/**
|
||||
* Updates the document title
|
||||
*
|
||||
* @param {String} title - the new title of the document
|
||||
*/
|
||||
return function updateTitle (title = document.title) {
|
||||
document.title = title
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import install from './shared/plugin'
|
||||
import { version } from '../package.json'
|
||||
import install from './shared/plugin'
|
||||
|
||||
install.version = version
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import titleGenerator from './generators/titleGenerator'
|
||||
import attrsGenerator from './generators/attrsGenerator'
|
||||
import tagGenerator from './generators/tagGenerator'
|
||||
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
|
||||
|
||||
export default function _generateServerInjector (options = {}) {
|
||||
/**
|
||||
* Converts a meta info property to one that can be stringified on the server
|
||||
*
|
||||
* @param {String} type - the type of data to convert
|
||||
* @param {(String|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
return function generateServerInjector (type, data) {
|
||||
switch (type) {
|
||||
case 'title':
|
||||
return titleGenerator(options)(type, data)
|
||||
case 'htmlAttrs':
|
||||
case 'bodyAttrs':
|
||||
case 'headAttrs':
|
||||
return attrsGenerator(options)(type, data)
|
||||
default:
|
||||
return tagGenerator(options)(type, data)
|
||||
}
|
||||
/**
|
||||
* Converts a meta info property to one that can be stringified on the server
|
||||
*
|
||||
* @param {String} type - the type of data to convert
|
||||
* @param {(String|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
|
||||
export default function generateServerInjector(options, type, data) {
|
||||
if (type === 'title') {
|
||||
return titleGenerator(options, type, data)
|
||||
}
|
||||
|
||||
if (['htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||
return attributeGenerator(options, type, data)
|
||||
}
|
||||
|
||||
return tagGenerator(options, type, data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Generates tag attributes for use on the server.
|
||||
*
|
||||
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
||||
* @param {Object} data - the attributes to generate
|
||||
* @return {Object} - the attribute generator
|
||||
*/
|
||||
export default function attributeGenerator({ attribute } = {}, type, data) {
|
||||
return {
|
||||
text() {
|
||||
let attributeStr = ''
|
||||
const watchedAttrs = []
|
||||
|
||||
for (const attr in data) {
|
||||
if (data.hasOwnProperty(attr)) {
|
||||
watchedAttrs.push(attr)
|
||||
|
||||
attributeStr += typeof data[attr] !== 'undefined'
|
||||
? `${attr}="${data[attr]}"`
|
||||
: attr
|
||||
|
||||
attributeStr += ' '
|
||||
}
|
||||
}
|
||||
|
||||
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
|
||||
return attributeStr
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export default function _attrsGenerator (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Generates tag attributes for use on the server.
|
||||
*
|
||||
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
||||
* @param {Object} data - the attributes to generate
|
||||
* @return {Object} - the attribute generator
|
||||
*/
|
||||
return function attrsGenerator (type, data) {
|
||||
return {
|
||||
text () {
|
||||
let attributeStr = ''
|
||||
let watchedAttrs = []
|
||||
for (let attr in data) {
|
||||
if (data.hasOwnProperty(attr)) {
|
||||
watchedAttrs.push(attr)
|
||||
attributeStr += `${
|
||||
typeof data[attr] !== 'undefined'
|
||||
? `${attr}="${data[attr]}"`
|
||||
: attr
|
||||
} `
|
||||
}
|
||||
}
|
||||
attributeStr += `${attribute}="${watchedAttrs.join(',')}"`
|
||||
return attributeStr.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as attributeGenerator } from './attribute'
|
||||
export { default as titleGenerator } from './title'
|
||||
export { default as tagGenerator } from './tag'
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Generates meta, base, link, style, script, noscript tags for use on the server
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - the tag generator
|
||||
*/
|
||||
export default function tagGenerator({ attribute, tagIDKeyName } = {}, type, tags) {
|
||||
return {
|
||||
text({ body = false } = {}) {
|
||||
// build a string containing all tags of this type
|
||||
return tags.reduce((tagsStr, tag) => {
|
||||
if (Object.keys(tag).length === 0) {
|
||||
return tagsStr // Bail on empty tag object
|
||||
}
|
||||
|
||||
if (Boolean(tag.body) !== body) {
|
||||
return tagsStr
|
||||
}
|
||||
|
||||
// build a string containing all attributes of this tag
|
||||
const attrs = Object.keys(tag).reduce((attrsStr, attr) => {
|
||||
// these attributes are treated as children on the tag
|
||||
if (['innerHTML', 'cssText', 'once'].includes(attr)) {
|
||||
return attrsStr
|
||||
}
|
||||
|
||||
// these form the attribute list for this tag
|
||||
let prefix = ''
|
||||
if ([tagIDKeyName, 'body'].includes(attr)) {
|
||||
prefix = 'data-'
|
||||
}
|
||||
|
||||
return typeof tag[attr] === 'undefined'
|
||||
? `${attrsStr} ${prefix}${attr}`
|
||||
: `${attrsStr} ${prefix}${attr}="${tag[attr]}"`
|
||||
}, '')
|
||||
|
||||
// grab child content from one of these attributes, if possible
|
||||
const content = tag.innerHTML || tag.cssText || ''
|
||||
|
||||
// generate tag exactly without any other redundant attribute
|
||||
const observeTag = tag.once
|
||||
? ''
|
||||
: `${attribute}="true"`
|
||||
|
||||
// these tags have no end tag
|
||||
const hasEndTag = !['base', 'meta', 'link'].includes(type)
|
||||
|
||||
// these tag types will have content inserted
|
||||
const hasContent = hasEndTag && ['noscript', 'script', 'style'].includes(type)
|
||||
|
||||
// the final string for this specific tag
|
||||
return !hasContent
|
||||
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
|
||||
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
export default function _tagGenerator (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Generates meta, base, link, style, script, noscript tags for use on the server
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - the tag generator
|
||||
*/
|
||||
return function tagGenerator (type, tags) {
|
||||
return {
|
||||
text ({ body = false } = {}) {
|
||||
// build a string containing all tags of this type
|
||||
return tags.reduce((tagsStr, tag) => {
|
||||
if (Object.keys(tag).length === 0) return tagsStr // Bail on empty tag object
|
||||
if (!!tag.body !== body) return tagsStr
|
||||
// build a string containing all attributes of this tag
|
||||
const attrs = Object.keys(tag).reduce((attrsStr, attr) => {
|
||||
switch (attr) {
|
||||
// these attributes are treated as children on the tag
|
||||
case 'innerHTML':
|
||||
case 'cssText':
|
||||
case 'once':
|
||||
return attrsStr
|
||||
// these form the attribute list for this tag
|
||||
default:
|
||||
if ([options.tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
||||
return `${attrsStr} data-${attr}="${tag[attr]}"`
|
||||
}
|
||||
return typeof tag[attr] === 'undefined'
|
||||
? `${attrsStr} ${attr}`
|
||||
: `${attrsStr} ${attr}="${tag[attr]}"`
|
||||
}
|
||||
}, '').trim()
|
||||
|
||||
// grab child content from one of these attributes, if possible
|
||||
const content = tag.innerHTML || tag.cssText || ''
|
||||
|
||||
// generate tag exactly without any other redundant attribute
|
||||
const observeTag = tag.once
|
||||
? ''
|
||||
: `${attribute}="true" `
|
||||
|
||||
// these tags have no end tag
|
||||
const hasEndTag = !['base', 'meta', 'link'].includes(type)
|
||||
|
||||
// these tag types will have content inserted
|
||||
const hasContent = hasEndTag && ['noscript', 'script', 'style'].includes(type)
|
||||
|
||||
// the final string for this specific tag
|
||||
return !hasContent
|
||||
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
|
||||
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generates title output for the server
|
||||
*
|
||||
* @param {'title'} type - the string "title"
|
||||
* @param {String} data - the title text
|
||||
* @return {Object} - the title generator
|
||||
*/
|
||||
export default function titleGenerator({ attribute } = {}, type, data) {
|
||||
return {
|
||||
text() {
|
||||
return `<${type} ${attribute}="true">${data}</${type}>`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export default function _titleGenerator (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Generates title output for the server
|
||||
*
|
||||
* @param {'title'} type - the string "title"
|
||||
* @param {String} data - the title text
|
||||
* @return {Object} - the title generator
|
||||
*/
|
||||
return function titleGenerator (type, data) {
|
||||
return {
|
||||
text () {
|
||||
return `<${type} ${attribute}="true">${data}</${type}>`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
export default function _inject (options = {}) {
|
||||
export default 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
|
||||
@@ -9,17 +9,18 @@ export default 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
|
||||
const info = getMetaInfo(options)(this.$root)
|
||||
const metaInfo = getMetaInfo(options, this.$root)
|
||||
|
||||
// generate server injectors
|
||||
for (let key in info) {
|
||||
if (info.hasOwnProperty(key) && key !== 'titleTemplate' && key !== 'titleChunk') {
|
||||
info[key] = generateServerInjector(options)(key, info[key])
|
||||
for (const key in metaInfo) {
|
||||
if (!['titleTemplate', 'titleChunk'].includes(key) && metaInfo.hasOwnProperty(key)) {
|
||||
metaInfo[key] = generateServerInjector(options, key, metaInfo[key])
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
return metaInfo
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import inject from '../server/inject'
|
||||
import refresh from '../client/refresh'
|
||||
|
||||
export default function _$meta (options = {}) {
|
||||
export default function _$meta(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 {
|
||||
inject: inject(options).bind(this),
|
||||
refresh: refresh(options).bind(this)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import uniqBy from './uniqBy'
|
||||
import uniqueId from 'lodash.uniqueid'
|
||||
import uniqBy from './uniqBy'
|
||||
|
||||
/**
|
||||
* Returns the `opts.option` $option value of the given `opts.component`.
|
||||
@@ -10,21 +10,22 @@ import uniqueId from 'lodash.uniqueid'
|
||||
*
|
||||
* @param {Object} opts - options
|
||||
* @param {Object} opts.component - Vue component to fetch option data from
|
||||
* @param {String} opts.option - what option to look for
|
||||
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
||||
* @param {String} opts.keyName - the name of the option to look for
|
||||
* @param {Object} [result={}] - result so far
|
||||
* @return {Object} result - final aggregated result
|
||||
*/
|
||||
export default function getComponentOption (opts, result = {}) {
|
||||
const { component, option, deep, arrayMerge, metaTemplateKeyName, tagIDKeyName, contentKeyName } = opts
|
||||
export default function getComponentOption({ component, deep, arrayMerge, keyName, metaTemplateKeyName, tagIDKeyName, contentKeyName } = {}, result = {}) {
|
||||
const { $options } = component
|
||||
|
||||
if (component._inactive) return result
|
||||
if (component._inactive) {
|
||||
return result
|
||||
}
|
||||
|
||||
// only collect option data if it exists
|
||||
if (typeof $options[option] !== 'undefined' && $options[option] !== null) {
|
||||
let data = $options[option]
|
||||
if (typeof $options[keyName] !== 'undefined' && $options[keyName] !== null) {
|
||||
let data = $options[keyName]
|
||||
|
||||
// if option is a function, replace it with it's result
|
||||
if (typeof data === 'function') {
|
||||
@@ -44,14 +45,15 @@ export default function getComponentOption (opts, result = {}) {
|
||||
component.$children.forEach((childComponent) => {
|
||||
result = getComponentOption({
|
||||
component: childComponent,
|
||||
option,
|
||||
keyName,
|
||||
deep,
|
||||
arrayMerge
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
if (metaTemplateKeyName && result.hasOwnProperty('meta')) {
|
||||
result.meta = Object.keys(result.meta).map(metaKey => {
|
||||
result.meta = Object.keys(result.meta).map((metaKey) => {
|
||||
const metaObject = result.meta[metaKey]
|
||||
if (!metaObject.hasOwnProperty(metaTemplateKeyName) || !metaObject.hasOwnProperty(contentKeyName) || typeof metaObject[metaTemplateKeyName] === 'undefined') {
|
||||
return result.meta[metaKey]
|
||||
|
||||
+128
-121
@@ -3,7 +3,7 @@ import isPlainObject from 'lodash.isplainobject'
|
||||
import isArray from './isArray'
|
||||
import getComponentOption from './getComponentOption'
|
||||
|
||||
const escapeHTML = (str) => typeof window === 'undefined'
|
||||
const escapeHTML = str => typeof window === 'undefined'
|
||||
// server-side escape sequence
|
||||
? String(str)
|
||||
.replace(/&/g, '&')
|
||||
@@ -19,138 +19,145 @@ const escapeHTML = (str) => typeof window === 'undefined'
|
||||
.replace(/"/g, '\u0022')
|
||||
.replace(/'/g, '\u0027')
|
||||
|
||||
export default function _getMetaInfo (options = {}) {
|
||||
const { keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = options
|
||||
/**
|
||||
* Returns the correct meta info for the given component
|
||||
* (child components will overwrite parent meta info)
|
||||
*
|
||||
* @param {Object} component - the Vue instance to get meta info from
|
||||
* @return {Object} - returned meta info
|
||||
*/
|
||||
return function getMetaInfo (component) {
|
||||
// set some sane defaults
|
||||
const defaultInfo = {
|
||||
title: '',
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
bodyAttrs: {},
|
||||
headAttrs: {},
|
||||
meta: [],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
}
|
||||
const applyTemplate = (component, template, chunk) =>
|
||||
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
let info = getComponentOption({
|
||||
component,
|
||||
option: keyName,
|
||||
deep: true,
|
||||
metaTemplateKeyName,
|
||||
tagIDKeyName,
|
||||
contentKeyName,
|
||||
arrayMerge (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
|
||||
// note the use of "for in" - we are looping through arrays here, not
|
||||
// plain objects
|
||||
const destination = []
|
||||
for (let targetIndex in target) {
|
||||
const targetItem = target[targetIndex]
|
||||
let shared = false
|
||||
for (let sourceIndex in source) {
|
||||
const sourceItem = source[sourceIndex]
|
||||
if (targetItem[tagIDKeyName] && targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
|
||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||
if (targetTemplate && !sourceTemplate) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component)(targetTemplate)(sourceItem[contentKeyName])
|
||||
}
|
||||
// If template defined in child but content in parent
|
||||
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component)(sourceTemplate)(targetItem[contentKeyName])
|
||||
delete sourceItem[metaTemplateKeyName]
|
||||
}
|
||||
shared = true
|
||||
break
|
||||
/**
|
||||
* Returns the correct meta info for the given component
|
||||
* (child components will overwrite parent meta info)
|
||||
*
|
||||
* @param {Object} component - the Vue instance to get meta info from
|
||||
* @return {Object} - returned meta info
|
||||
*/
|
||||
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, component) {
|
||||
// set some sane defaults
|
||||
const defaultInfo = {
|
||||
title: '',
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
bodyAttrs: {},
|
||||
headAttrs: {},
|
||||
meta: [],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
}
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
let info = getComponentOption({
|
||||
deep: true,
|
||||
component,
|
||||
keyName,
|
||||
metaTemplateKeyName,
|
||||
tagIDKeyName,
|
||||
contentKeyName,
|
||||
arrayMerge(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
|
||||
// note the use of "for in" - we are looping through arrays here, not
|
||||
// plain objects
|
||||
const destination = []
|
||||
|
||||
for (const targetIndex in target) {
|
||||
const targetItem = target[targetIndex]
|
||||
let shared = false
|
||||
|
||||
for (const sourceIndex in source) {
|
||||
const sourceItem = source[sourceIndex]
|
||||
|
||||
if (targetItem[tagIDKeyName] && targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
|
||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||
|
||||
if (targetTemplate && !sourceTemplate) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component, targetTemplate, sourceItem[contentKeyName])
|
||||
}
|
||||
}
|
||||
|
||||
if (!shared) {
|
||||
destination.push(targetItem)
|
||||
// If template defined in child but content in parent
|
||||
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component, sourceTemplate, targetItem[contentKeyName])
|
||||
delete sourceItem[metaTemplateKeyName]
|
||||
}
|
||||
|
||||
shared = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return destination.concat(source)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove all "template" tags from meta
|
||||
|
||||
// backup the title chunk in case user wants access to it
|
||||
if (info.title) {
|
||||
info.titleChunk = info.title
|
||||
}
|
||||
|
||||
// replace title with populated template
|
||||
if (info.titleTemplate) {
|
||||
info.title = applyTemplate(component)(info.titleTemplate)(info.titleChunk || '')
|
||||
}
|
||||
|
||||
// convert base tag to an array so it can be handled the same way
|
||||
// as the other tags
|
||||
if (info.base) {
|
||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||
}
|
||||
|
||||
const ref = info.__dangerouslyDisableSanitizers
|
||||
const refByTagID = info.__dangerouslyDisableSanitizersByTagID
|
||||
|
||||
// sanitizes potentially dangerous characters
|
||||
const escape = (info) => Object.keys(info).reduce((escaped, key) => {
|
||||
let isDisabled = ref && ref.indexOf(key) > -1
|
||||
const tagID = info[tagIDKeyName]
|
||||
if (!isDisabled && tagID) {
|
||||
isDisabled = refByTagID && refByTagID[tagID] && refByTagID[tagID].indexOf(key) > -1
|
||||
}
|
||||
const val = info[key]
|
||||
escaped[key] = val
|
||||
if (key === '__dangerouslyDisableSanitizers' || key === '__dangerouslyDisableSanitizersByTagID') {
|
||||
return escaped
|
||||
}
|
||||
if (!isDisabled) {
|
||||
if (typeof val === 'string') {
|
||||
escaped[key] = escapeHTML(val)
|
||||
} else if (isPlainObject(val)) {
|
||||
escaped[key] = escape(val)
|
||||
} else if (isArray(val)) {
|
||||
escaped[key] = val.map(escape)
|
||||
} else {
|
||||
escaped[key] = val
|
||||
if (!shared) {
|
||||
destination.push(targetItem)
|
||||
}
|
||||
}
|
||||
|
||||
return destination.concat(source)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove all "template" tags from meta
|
||||
|
||||
// backup the title chunk in case user wants access to it
|
||||
if (info.title) {
|
||||
info.titleChunk = info.title
|
||||
}
|
||||
|
||||
// replace title with populated template
|
||||
if (info.titleTemplate) {
|
||||
info.title = applyTemplate(component, info.titleTemplate, info.titleChunk || '')
|
||||
}
|
||||
|
||||
// convert base tag to an array so it can be handled the same way
|
||||
// as the other tags
|
||||
if (info.base) {
|
||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||
}
|
||||
|
||||
const ref = info.__dangerouslyDisableSanitizers
|
||||
const refByTagID = info.__dangerouslyDisableSanitizersByTagID
|
||||
|
||||
// sanitizes potentially dangerous characters
|
||||
const escape = info => Object.keys(info).reduce((escaped, key) => {
|
||||
let isDisabled = ref && ref.indexOf(key) > -1
|
||||
const tagID = info[tagIDKeyName]
|
||||
|
||||
if (!isDisabled && tagID) {
|
||||
isDisabled = refByTagID && refByTagID[tagID] && refByTagID[tagID].indexOf(key) > -1
|
||||
}
|
||||
|
||||
const val = info[key]
|
||||
escaped[key] = val
|
||||
|
||||
if (key === '__dangerouslyDisableSanitizers' || key === '__dangerouslyDisableSanitizersByTagID') {
|
||||
return escaped
|
||||
}
|
||||
|
||||
if (!isDisabled) {
|
||||
if (typeof val === 'string') {
|
||||
escaped[key] = escapeHTML(val)
|
||||
} else if (isPlainObject(val)) {
|
||||
escaped[key] = escape(val)
|
||||
} else if (isArray(val)) {
|
||||
escaped[key] = val.map(escape)
|
||||
} else {
|
||||
escaped[key] = val
|
||||
}
|
||||
} else {
|
||||
escaped[key] = val
|
||||
}
|
||||
|
||||
return escaped
|
||||
}, {})
|
||||
return escaped
|
||||
}, {})
|
||||
|
||||
// merge with defaults
|
||||
info = deepmerge(defaultInfo, info)
|
||||
// merge with defaults
|
||||
info = deepmerge(defaultInfo, info)
|
||||
|
||||
// begin sanitization
|
||||
info = escape(info)
|
||||
// begin sanitization
|
||||
info = escape(info)
|
||||
|
||||
return info
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
const applyTemplate = component => template => chunk =>
|
||||
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @param {any} arr - the object to check
|
||||
* @return {Boolean} - true if `arr` is an array
|
||||
*/
|
||||
export default function isArray (arr) {
|
||||
export default function isArray(arr) {
|
||||
return Array.isArray
|
||||
? Array.isArray(arr)
|
||||
: Object.prototype.toString.call(arr) === '[object Array]'
|
||||
|
||||
+89
-34
@@ -1,6 +1,5 @@
|
||||
import assign from 'object-assign'
|
||||
import $meta from './$meta'
|
||||
import batchUpdate from '../client/batchUpdate'
|
||||
import $meta from './$meta'
|
||||
|
||||
import {
|
||||
VUE_META_KEY_NAME,
|
||||
@@ -19,7 +18,7 @@ if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') {
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
export default function VueMeta (Vue, options = {}) {
|
||||
export default function VueMeta(Vue, options = {}) {
|
||||
// set some default options
|
||||
const defaultOptions = {
|
||||
keyName: VUE_META_KEY_NAME,
|
||||
@@ -29,8 +28,15 @@ export default function VueMeta (Vue, options = {}) {
|
||||
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
||||
}
|
||||
|
||||
// combine options
|
||||
options = assign(defaultOptions, options)
|
||||
options = typeof options === 'object' ? options : {}
|
||||
|
||||
for (const key in defaultOptions) {
|
||||
if (!options[key]) {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
}
|
||||
|
||||
// bind the $meta method to this component instance
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
@@ -38,66 +44,115 @@ export default function VueMeta (Vue, options = {}) {
|
||||
// store an id to keep track of DOM updates
|
||||
let batchID = null
|
||||
|
||||
const triggerUpdate = (vm) => {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => vm.$meta().refresh())
|
||||
}
|
||||
|
||||
// watch for client side component updates
|
||||
Vue.mixin({
|
||||
beforeCreate () {
|
||||
beforeCreate() {
|
||||
// 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 (typeof this.$options[options.keyName] !== 'undefined') {
|
||||
if (typeof this.$options[options.keyName] !== 'undefined' && this.$options[options.keyName] !== null) {
|
||||
this._hasMetaInfo = true
|
||||
}
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (typeof this.$options[options.keyName] === 'function') {
|
||||
if (typeof this.$options.computed === 'undefined') {
|
||||
this.$options.computed = {}
|
||||
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (typeof this.$options[options.keyName] === 'function') {
|
||||
if (typeof this.$options.computed === 'undefined') {
|
||||
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)
|
||||
this.$options.created = this.$options.created || []
|
||||
this.$options.created.push(() => {
|
||||
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
['activated', 'deactivated', 'beforeMount'].forEach((lifecycleHook) => {
|
||||
this.$options[lifecycleHook] = this.$options[lifecycleHook] || []
|
||||
this.$options[lifecycleHook].push(() => triggerUpdate(this))
|
||||
})
|
||||
|
||||
// do not trigger refresh on the server side
|
||||
if (!this.$isServer) {
|
||||
// re-render meta data when returning from a child component to parent
|
||||
this.$options.destroyed = this.$options.destroyed || []
|
||||
this.$options.destroyed.push(() => {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
|
||||
if (!this.$parent) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerUpdate(this)
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
this.$options.computed.$metaInfo = this.$options[options.keyName]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
}
|
||||
/* Not yet removed
|
||||
created() {
|
||||
// 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)
|
||||
if (!this.$isServer && this.$metaInfo) {
|
||||
this.$watch('$metaInfo', () => {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
})
|
||||
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||
}
|
||||
|
||||
},
|
||||
activated () {
|
||||
activated() {
|
||||
if (this._hasMetaInfo) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
deactivated () {
|
||||
deactivated() {
|
||||
if (this._hasMetaInfo) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
beforeMount() {
|
||||
if (this._hasMetaInfo) {
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
destroyed() {
|
||||
// do not trigger refresh on the server side
|
||||
if (this.$isServer) return
|
||||
if (this.$isServer) {
|
||||
return
|
||||
}
|
||||
|
||||
// re-render meta data when returning from a child component to parent
|
||||
if (this._hasMetaInfo) {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) return
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
if (!this.$parent) return
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
|
||||
if (!this.$parent) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerUpdate(this)
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
}/**/
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function uniqBy (inputArray, predicate) {
|
||||
export default function uniqBy(inputArray, predicate) {
|
||||
return inputArray
|
||||
.filter((x, i, arr) => i === arr.length - 1
|
||||
? true
|
||||
|
||||
Reference in New Issue
Block a user