mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-19 16:00:33 +03:00
feat: convert to ts (wip)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { h, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { renderMeta } from './render'
|
||||
import { getCurrentManager } from './useApi'
|
||||
import { MetainfoActive } from './types'
|
||||
|
||||
export interface MetainfoProps {
|
||||
metainfo: MetainfoActive
|
||||
}
|
||||
|
||||
export function addVnode(targets: any, target: string, vnode: VNode) {
|
||||
if (!targets[target]) {
|
||||
targets[target] = []
|
||||
}
|
||||
|
||||
targets[target].push(vnode)
|
||||
}
|
||||
|
||||
export const MetainfoImpl = defineComponent({
|
||||
name: 'Metainfo',
|
||||
props: {
|
||||
metainfo: {
|
||||
type: Object as PropType<MetainfoActive>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup({ metainfo }, { slots }) {
|
||||
return () => {
|
||||
const targets: any = {}
|
||||
|
||||
const manager = getCurrentManager()
|
||||
|
||||
for (const key in metainfo) {
|
||||
const config = manager.config[key] || {}
|
||||
|
||||
const vnodes = renderMeta(
|
||||
{ metainfo, slots },
|
||||
key,
|
||||
metainfo[key],
|
||||
config
|
||||
)
|
||||
let defaultTarget =
|
||||
(key !== 'base' && metainfo[key].target) || config.target || 'head'
|
||||
|
||||
if (isArray(vnodes)) {
|
||||
for (const { target, vnode } of vnodes) {
|
||||
addVnode(targets, target || defaultTarget, vnode)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const { target, vnode } = vnodes
|
||||
addVnode(targets, target || defaultTarget, vnode)
|
||||
}
|
||||
|
||||
// console.log('TARGETS', targets)
|
||||
return Object.keys(targets).map(target => {
|
||||
return h(Teleport, { to: target }, targets[target])
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const Metainfo = (MetainfoImpl as any) as {
|
||||
new (): {
|
||||
$props: VNodeProps & MetainfoProps
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { toArray } from '../utils/array'
|
||||
import { querySelector, removeAttribute } from '../utils/elements'
|
||||
|
||||
const callbacks = []
|
||||
|
||||
export function isDOMLoaded (d) {
|
||||
return (d || document).readyState !== 'loading'
|
||||
}
|
||||
|
||||
export function isDOMComplete (d) {
|
||||
return (d || document).readyState === 'complete'
|
||||
}
|
||||
|
||||
export function waitDOMLoaded () {
|
||||
if (isDOMLoaded()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve))
|
||||
}
|
||||
|
||||
export function addCallback (query, callback) {
|
||||
if (arguments.length === 1) {
|
||||
callback = query
|
||||
query = ''
|
||||
}
|
||||
|
||||
callbacks.push([query, callback])
|
||||
}
|
||||
|
||||
export function addCallbacks ({ tagIDKeyName }, type, tags, autoAddListeners) {
|
||||
let hasAsyncCallback = false
|
||||
|
||||
tags.forEach((tag) => {
|
||||
if (!tag[tagIDKeyName] || !tag.callback) {
|
||||
return
|
||||
}
|
||||
|
||||
hasAsyncCallback = true
|
||||
addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback)
|
||||
})
|
||||
|
||||
if (!autoAddListeners || !hasAsyncCallback) {
|
||||
return hasAsyncCallback
|
||||
}
|
||||
|
||||
return addListeners()
|
||||
}
|
||||
|
||||
export function addListeners () {
|
||||
if (isDOMComplete()) {
|
||||
applyCallbacks()
|
||||
return
|
||||
}
|
||||
|
||||
// Instead of using a MutationObserver, we just apply
|
||||
/* istanbul ignore next */
|
||||
document.onreadystatechange = () => {
|
||||
applyCallbacks()
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCallbacks (matchElement) {
|
||||
callbacks.forEach((args) => {
|
||||
// do not use destructuring for args, it increases transpiled size
|
||||
// due to var checks while we are guaranteed the structure of the cb
|
||||
const query = args[0]
|
||||
const callback = args[1]
|
||||
const selector = `${query}[onload="this.__vm_l=1"]`
|
||||
|
||||
let elements = []
|
||||
if (!matchElement) {
|
||||
elements = toArray(querySelector(selector))
|
||||
}
|
||||
|
||||
if (matchElement && matchElement.matches(selector)) {
|
||||
elements = [matchElement]
|
||||
}
|
||||
|
||||
elements.forEach((element) => {
|
||||
/* __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) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
removeAttribute(element, '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()
|
||||
return
|
||||
}
|
||||
|
||||
if (!element.__vm_ev) {
|
||||
element.__vm_ev = true
|
||||
|
||||
element.addEventListener('load', onload)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { clientSequences } from '../shared/escaping'
|
||||
import { rootConfigKey } from '../shared/constants'
|
||||
import { showWarningNotSupported } from '../shared/log'
|
||||
import { getComponentMetaInfo } from '../shared/getComponentOption'
|
||||
import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app'
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import { isFunction } from '../utils/is-type'
|
||||
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||
|
||||
/**
|
||||
* When called, will update the current meta info with new meta info.
|
||||
* Useful when updating meta info as the result of an asynchronous
|
||||
* action that resolves after the initial render takes place.
|
||||
*
|
||||
* Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion
|
||||
* to implement this method.
|
||||
*
|
||||
* @return {Object} - new meta info
|
||||
*/
|
||||
export default function refresh (rootVm, options) {
|
||||
options = options || {}
|
||||
|
||||
// make sure vue-meta was initiated
|
||||
if (!rootVm[rootConfigKey]) {
|
||||
showWarningNotSupported()
|
||||
return {}
|
||||
}
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
const rawInfo = getComponentMetaInfo(options, rootVm)
|
||||
|
||||
const metaInfo = getMetaInfo(options, rawInfo, clientSequences, rootVm)
|
||||
|
||||
const { appId } = rootVm[rootConfigKey]
|
||||
let tags = updateClientMetaInfo(appId, options, metaInfo)
|
||||
|
||||
// emit "event" with new info
|
||||
if (tags && isFunction(metaInfo.changed)) {
|
||||
metaInfo.changed(metaInfo, tags.tagsAdded, tags.tagsRemoved)
|
||||
|
||||
tags = {
|
||||
addedTags: tags.tagsAdded,
|
||||
removedTags: tags.tagsRemoved
|
||||
}
|
||||
}
|
||||
|
||||
const appsMetaInfo = getAppsMetaInfo()
|
||||
if (appsMetaInfo) {
|
||||
for (const additionalAppId in appsMetaInfo) {
|
||||
updateClientMetaInfo(additionalAppId, options, appsMetaInfo[additionalAppId])
|
||||
delete appsMetaInfo[additionalAppId]
|
||||
}
|
||||
clearAppsMetaInfo(true)
|
||||
}
|
||||
|
||||
return {
|
||||
vm: rootVm,
|
||||
metaInfo: metaInfo, // eslint-disable-line object-shorthand
|
||||
tags
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { rootConfigKey } from '../shared/constants'
|
||||
|
||||
// store an id to keep track of DOM updates
|
||||
let batchId = null
|
||||
|
||||
export function triggerUpdate ({ debounceWait }, rootVm, hookName) {
|
||||
// if an update was triggered during initialization or when an update was triggered by the
|
||||
// metaInfo watcher, set initialized to null
|
||||
// then we keep falsy value but know we need to run a triggerUpdate after initialization
|
||||
if (!rootVm[rootConfigKey].initialized && (rootVm[rootConfigKey].initializing || hookName === 'watcher')) {
|
||||
rootVm[rootConfigKey].initialized = null
|
||||
}
|
||||
|
||||
if (rootVm[rootConfigKey].initialized && !rootVm[rootConfigKey].pausing) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
// eslint-disable-next-line no-void
|
||||
batchUpdate(() => void rootVm.$meta().refresh(), debounceWait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a batched update.
|
||||
*
|
||||
* @param {(null|Number)} id - the ID of this update
|
||||
* @param {Function} callback - the update to perform
|
||||
* @return {Number} id - a new ID
|
||||
*/
|
||||
export function batchUpdate (callback, timeout) {
|
||||
timeout = timeout === undefined ? 10 : timeout
|
||||
|
||||
if (!timeout) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(batchId)
|
||||
batchId = setTimeout(() => {
|
||||
callback()
|
||||
}, timeout)
|
||||
|
||||
return batchId
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys, tagsSupportingOnload } from '../shared/constants'
|
||||
import { isArray } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { getTag, removeAttribute } from '../utils/elements'
|
||||
import { addCallbacks, addListeners } from './load'
|
||||
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @param {Object} newInfo - the meta info to update to
|
||||
*/
|
||||
export default function updateClientMetaInfo (appId, options, newInfo) {
|
||||
options = options || {}
|
||||
const { ssrAttribute, ssrAppId } = options
|
||||
|
||||
// only cache tags for current update
|
||||
const tags = {}
|
||||
|
||||
const htmlTag = getTag(tags, 'html')
|
||||
|
||||
// if this is a server render, then dont update
|
||||
if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) {
|
||||
// remove the server render attribute so we can update on (next) changes
|
||||
removeAttribute(htmlTag, ssrAttribute)
|
||||
|
||||
// add load callbacks if the
|
||||
let addLoadListeners = false
|
||||
tagsSupportingOnload.forEach((type) => {
|
||||
if (newInfo[type] && addCallbacks(options, type, newInfo[type])) {
|
||||
addLoadListeners = true
|
||||
}
|
||||
})
|
||||
|
||||
if (addLoadListeners) {
|
||||
addListeners()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// initialize tracked changes
|
||||
const tagsAdded = {}
|
||||
const tagsRemoved = {}
|
||||
|
||||
for (const type in newInfo) {
|
||||
// ignore these
|
||||
if (includes(metaInfoOptionKeys, type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'title') {
|
||||
// update the title
|
||||
updateTitle(newInfo.title)
|
||||
continue
|
||||
}
|
||||
|
||||
if (includes(metaInfoAttributeKeys, type)) {
|
||||
const tagName = type.substr(0, 4)
|
||||
updateAttribute(appId, options, type, newInfo[type], getTag(tags, tagName))
|
||||
continue
|
||||
}
|
||||
|
||||
// tags should always be an array, ignore if it isnt
|
||||
if (!isArray(newInfo[type])) {
|
||||
continue
|
||||
}
|
||||
|
||||
const { oldTags, newTags } = updateTag(
|
||||
appId,
|
||||
options,
|
||||
type,
|
||||
newInfo[type],
|
||||
getTag(tags, 'head'),
|
||||
getTag(tags, 'body')
|
||||
)
|
||||
|
||||
if (newTags.length) {
|
||||
tagsAdded[type] = newTags
|
||||
tagsRemoved[type] = oldTags
|
||||
}
|
||||
}
|
||||
|
||||
return { tagsAdded, tagsRemoved }
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { booleanHtmlAttributes } from '../../shared/constants'
|
||||
import { includes } from '../../utils/array'
|
||||
import { removeAttribute } from '../../utils/elements'
|
||||
|
||||
// keep a local map of attribute values
|
||||
// instead of adding it to the html
|
||||
export const attributeMap = {}
|
||||
|
||||
/**
|
||||
* 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 (appId, options, type, attrs, tag) {
|
||||
const { attribute } = options || {}
|
||||
|
||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||
if (vueMetaAttrString) {
|
||||
attributeMap[type] = JSON.parse(decodeURI(vueMetaAttrString))
|
||||
removeAttribute(tag, attribute)
|
||||
}
|
||||
|
||||
const data = attributeMap[type] || {}
|
||||
|
||||
const toUpdate = []
|
||||
|
||||
// remove attributes from the map
|
||||
// which have been removed for this appId
|
||||
for (const attr in data) {
|
||||
if (data[attr] && appId in data[attr]) {
|
||||
toUpdate.push(attr)
|
||||
|
||||
if (!attrs[attr]) {
|
||||
delete data[attr][appId]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr in attrs) {
|
||||
const attrData = data[attr]
|
||||
|
||||
if (!attrData || attrData[appId] !== attrs[attr]) {
|
||||
toUpdate.push(attr)
|
||||
|
||||
if (attrs[attr]) {
|
||||
data[attr] = data[attr] || {}
|
||||
data[attr][appId] = attrs[attr]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of toUpdate) {
|
||||
const attrData = data[attr]
|
||||
|
||||
const attrValues = []
|
||||
for (const appId in attrData) {
|
||||
Array.prototype.push.apply(attrValues, [].concat(attrData[appId]))
|
||||
}
|
||||
|
||||
if (attrValues.length) {
|
||||
const attrValue = includes(booleanHtmlAttributes, attr) && attrValues.some(Boolean)
|
||||
? ''
|
||||
: attrValues.filter(Boolean).join(' ')
|
||||
|
||||
tag.setAttribute(attr, attrValue)
|
||||
} else {
|
||||
removeAttribute(tag, attr)
|
||||
}
|
||||
}
|
||||
|
||||
attributeMap[type] = data
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as updateAttribute } from './attribute'
|
||||
export { default as updateTitle } from './title'
|
||||
export { default as updateTag } from './tag'
|
||||
@@ -1,141 +0,0 @@
|
||||
import { booleanHtmlAttributes, commonDataAttributes, tagProperties } from '../../shared/constants'
|
||||
import { includes } from '../../utils/array'
|
||||
import { queryElements, getElementsKey } from '../../utils/elements.js'
|
||||
|
||||
/**
|
||||
* 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 (appId, options, type, tags, head, body) {
|
||||
const { attribute, tagIDKeyName } = options || {}
|
||||
|
||||
const dataAttributes = commonDataAttributes.slice()
|
||||
dataAttributes.push(tagIDKeyName)
|
||||
|
||||
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
|
||||
const found = []
|
||||
tags = tags.filter((x) => {
|
||||
const k = JSON.stringify(x)
|
||||
const res = !includes(found, k)
|
||||
found.push(k)
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
tags.forEach((tag) => {
|
||||
if (tag.skip) {
|
||||
return
|
||||
}
|
||||
|
||||
const newElement = document.createElement(type)
|
||||
|
||||
if (!tag.once) {
|
||||
newElement.setAttribute(attribute, appId)
|
||||
}
|
||||
|
||||
Object.keys(tag).forEach((attr) => {
|
||||
/* istanbul ignore next */
|
||||
if (includes(tagProperties, attr)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'innerHTML') {
|
||||
newElement.innerHTML = tag.innerHTML
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'json') {
|
||||
newElement.innerHTML = JSON.stringify(tag.json)
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'cssText') {
|
||||
if (newElement.styleSheet) {
|
||||
/* istanbul ignore next */
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'callback') {
|
||||
newElement.onload = () => tag[attr](newElement)
|
||||
return
|
||||
}
|
||||
|
||||
const _attr = includes(dataAttributes, attr)
|
||||
? `data-${attr}`
|
||||
: attr
|
||||
|
||||
const isBooleanAttribute = includes(booleanHtmlAttributes, attr)
|
||||
if (isBooleanAttribute && !tag[attr]) {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
if (hasEqualElement && (indexToDelete || indexToDelete === 0)) {
|
||||
oldElements.splice(indexToDelete, 1)
|
||||
} else {
|
||||
newElements.push(newElement)
|
||||
}
|
||||
})
|
||||
|
||||
const oldElements = []
|
||||
for (const type in currentElements) {
|
||||
Array.prototype.push.apply(oldElements, currentElements[type])
|
||||
}
|
||||
|
||||
// remove old elements
|
||||
oldElements.forEach((element) => {
|
||||
element.parentNode.removeChild(element)
|
||||
})
|
||||
|
||||
// insert new elements
|
||||
newElements.forEach((element) => {
|
||||
if (element.hasAttribute('data-body')) {
|
||||
body.appendChild(element)
|
||||
return
|
||||
}
|
||||
|
||||
if (element.hasAttribute('data-pbody')) {
|
||||
body.insertBefore(element, body.firstChild)
|
||||
return
|
||||
}
|
||||
|
||||
head.appendChild(element)
|
||||
})
|
||||
|
||||
return {
|
||||
oldTags: oldElements,
|
||||
newTags: newElements
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Updates the document title
|
||||
*
|
||||
* @param {String} title - the new title of the document
|
||||
*/
|
||||
export default function updateTitle (title) {
|
||||
if (!title && title !== '') {
|
||||
return
|
||||
}
|
||||
|
||||
document.title = title
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { isArray } from '@vue/shared'
|
||||
import { tags } from './config/tags'
|
||||
import { TODO } from './types'
|
||||
|
||||
export interface ConfigOption {
|
||||
tag?: string
|
||||
target?: string
|
||||
group?: boolean
|
||||
nameAttribute?: string
|
||||
contentAttribute?: string
|
||||
nameless?: boolean
|
||||
namespaced?: boolean
|
||||
namespacedAttribute?: boolean
|
||||
}
|
||||
|
||||
const defaultMapping: { [key: string]: ConfigOption } = {
|
||||
body: {
|
||||
tag: 'script',
|
||||
target: 'body',
|
||||
},
|
||||
base: {
|
||||
contentAttribute: 'href',
|
||||
},
|
||||
charset: {
|
||||
tag: 'meta',
|
||||
nameless: true,
|
||||
contentAttribute: 'charset',
|
||||
},
|
||||
description: {
|
||||
tag: 'meta',
|
||||
},
|
||||
og: {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta',
|
||||
nameAttribute: 'property',
|
||||
},
|
||||
twitter: {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta',
|
||||
},
|
||||
}
|
||||
|
||||
export { defaultMapping }
|
||||
|
||||
export function hasConfig(name: string): boolean {
|
||||
return !!tags[name] || !!defaultMapping[name]
|
||||
}
|
||||
|
||||
export function getConfigKey(
|
||||
name: string | Array<string>,
|
||||
key: string,
|
||||
config: TODO,
|
||||
dontLog?: boolean
|
||||
): any {
|
||||
if (!dontLog) {
|
||||
// console.log('getConfigKey', name, key, getConfigKey(name, key, config, true), config)
|
||||
}
|
||||
|
||||
if (config && key in config) {
|
||||
return config[key]
|
||||
}
|
||||
|
||||
if (isArray(name)) {
|
||||
for (const _name of name) {
|
||||
if (_name && _name in tags) {
|
||||
return tags[_name][key]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (name in tags) {
|
||||
const tag = tags[name]
|
||||
return tag[key]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
export interface TagConfig {
|
||||
nameAttribute?: string
|
||||
contentAttributes: boolean | Array<string>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const tags: { [key: string]: TagConfig } = {
|
||||
title: {
|
||||
contentAttributes: false,
|
||||
},
|
||||
base: {
|
||||
contentAttributes: ['href', 'target'],
|
||||
},
|
||||
meta: {
|
||||
nameAttribute: 'name',
|
||||
contentAttributes: ['content', 'name', 'http-equiv', 'charset'],
|
||||
},
|
||||
link: {
|
||||
contentAttributes: [
|
||||
'href',
|
||||
'crossorigin',
|
||||
'rel',
|
||||
'media',
|
||||
'integrity',
|
||||
'hreflang',
|
||||
'type',
|
||||
'referrerpolicy',
|
||||
'sizes',
|
||||
'imagesrcset',
|
||||
'imagesizes',
|
||||
'as',
|
||||
'color',
|
||||
],
|
||||
},
|
||||
style: {
|
||||
contentAttributes: ['media'],
|
||||
},
|
||||
script: {
|
||||
contentAttributes: [
|
||||
'src',
|
||||
'type',
|
||||
'nomodule',
|
||||
'async',
|
||||
'defer',
|
||||
'crossorigin',
|
||||
'integrity',
|
||||
'referrerpolicy',
|
||||
],
|
||||
},
|
||||
noscript: {
|
||||
contentAttributes: false,
|
||||
},
|
||||
}
|
||||
|
||||
export { tags }
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// Global compile-time constants
|
||||
declare var __DEV__: boolean
|
||||
@@ -1,33 +0,0 @@
|
||||
import { version } from '../package.json'
|
||||
import { showWarningNotSupportedInBrowserBundle } from './shared/log'
|
||||
import createMixin from './shared/mixin'
|
||||
import { setOptions } from './shared/options'
|
||||
import $meta from './shared/$meta'
|
||||
import generate from './server/generate'
|
||||
import { hasMetaInfo } from './shared/meta-helpers'
|
||||
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
function install (Vue, options) {
|
||||
if (Vue.__vuemeta_installed) {
|
||||
return
|
||||
}
|
||||
Vue.__vuemeta_installed = true
|
||||
|
||||
options = setOptions(options)
|
||||
|
||||
Vue.prototype.$meta = function () {
|
||||
return $meta.call(this, options)
|
||||
}
|
||||
|
||||
Vue.mixin(createMixin(Vue, options))
|
||||
}
|
||||
|
||||
export default {
|
||||
version,
|
||||
install,
|
||||
generate: (metaInfo, options) => process.server ? generate(metaInfo, options) : showWarningNotSupportedInBrowserBundle('generate'),
|
||||
hasMetaInfo
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createManager } from './manager'
|
||||
export * from './useApi'
|
||||
@@ -0,0 +1,4 @@
|
||||
import { markRaw, reactive } from 'vue'
|
||||
|
||||
export const shadow = markRaw({})
|
||||
export const active = reactive({})
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './globals'
|
||||
export * from './remove'
|
||||
export * from './set'
|
||||
export * from './update'
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setByObject } from './set'
|
||||
import { MetaContext } from '../types'
|
||||
|
||||
export function remove(context: MetaContext) {
|
||||
setByObject(context, {})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { clone } from '../utils'
|
||||
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
|
||||
|
||||
export function resolveActive(
|
||||
context: MetaContext,
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadowParent: ShadowNode,
|
||||
activeParent: ActiveNode
|
||||
) {
|
||||
let value
|
||||
|
||||
if (shadowParent[key].length > 1) {
|
||||
// Is this useful? Idea is to prevent the user from messing with these options by mistake
|
||||
const getShadow = () => Object.freeze(clone(shadowParent[key]))
|
||||
const getActive = () => Object.freeze(clone(activeParent[key]))
|
||||
|
||||
value = context.manager.resolver.resolve(
|
||||
key,
|
||||
pathSegments,
|
||||
getShadow,
|
||||
getActive
|
||||
)
|
||||
} else if (shadowParent[key].length) {
|
||||
value = shadowParent[key][0].value
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
delete activeParent[key]
|
||||
} else if (!hasOwn(activeParent, key) || activeParent[key] !== value) {
|
||||
activeParent[key] = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { isPlainObject, hasOwn } from '@vue/shared'
|
||||
import { shadow, active } from './globals'
|
||||
import { resolveActive } from './resolve'
|
||||
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
|
||||
|
||||
export function set(
|
||||
context: MetaContext,
|
||||
key: string,
|
||||
value: any,
|
||||
shadowParent: ShadowNode = shadow,
|
||||
activeParent: ActiveNode = active,
|
||||
pathSegments: PathSegments = []
|
||||
) {
|
||||
if (isPlainObject(value)) {
|
||||
// shadow & active should always be in sync
|
||||
// if not we have bigger fish to fry
|
||||
if (!shadowParent[key]) {
|
||||
shadowParent[key] = {}
|
||||
activeParent[key] = {}
|
||||
}
|
||||
|
||||
return setByObject(
|
||||
context,
|
||||
value,
|
||||
shadowParent[key],
|
||||
activeParent[key],
|
||||
pathSegments
|
||||
)
|
||||
}
|
||||
|
||||
let idx = -1
|
||||
if (!shadowParent[key]) {
|
||||
shadowParent[key] = []
|
||||
} else {
|
||||
// check if we already have a value listed for this element for this context
|
||||
idx = shadowParent[key].findIndex(
|
||||
({ context: $context }: { context: MetaContext }) => $context === context
|
||||
)
|
||||
}
|
||||
|
||||
// if this context/key combo exists but value is undefined, remove it
|
||||
if (idx > -1 && value === undefined) {
|
||||
shadowParent[key].splice(idx, 1)
|
||||
|
||||
// overwrite current value for context/key combo
|
||||
} else if (idx > -1) {
|
||||
shadowParent[key][idx].value = value
|
||||
|
||||
// new context/key combo so just add value
|
||||
} else if (value) {
|
||||
shadowParent[key].push({ context, value })
|
||||
}
|
||||
|
||||
resolveActive(context, key, pathSegments, shadowParent, activeParent)
|
||||
}
|
||||
|
||||
export function setByObject(
|
||||
context: MetaContext,
|
||||
value: any,
|
||||
shadowParent: ShadowNode = shadow,
|
||||
activeParent: ActiveNode = active,
|
||||
pathSegments: PathSegments = []
|
||||
) {
|
||||
// cleanup properties that no longer exists
|
||||
for (const key in shadowParent) {
|
||||
if (hasOwn(value, key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPlainObject(shadowParent[key])) {
|
||||
setByObject(context, {}, shadowParent[key], activeParent[key], [
|
||||
...pathSegments,
|
||||
key,
|
||||
])
|
||||
continue
|
||||
}
|
||||
|
||||
set(context, key, undefined, shadowParent, activeParent, [
|
||||
...pathSegments,
|
||||
key,
|
||||
])
|
||||
}
|
||||
|
||||
// set new values
|
||||
for (const key in value) {
|
||||
set(context, key, value[key], shadowParent, activeParent, [
|
||||
...pathSegments,
|
||||
key,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { shadow, active } from './globals'
|
||||
import { set } from './set'
|
||||
import { MetaContext, PathSegments, ShadowNode, ActiveNode } from '../types'
|
||||
|
||||
export function update(
|
||||
context: MetaContext,
|
||||
pathSegments: PathSegments,
|
||||
key: string,
|
||||
value: any
|
||||
) {
|
||||
let shadowParent: ShadowNode = shadow
|
||||
let activeParent: ActiveNode = active
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
shadowParent = shadowParent[segment]
|
||||
activeParent = activeParent[segment]
|
||||
}
|
||||
|
||||
set(context, key, value, shadowParent, activeParent)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { App } from 'vue'
|
||||
import { Metainfo } from './Metainfo'
|
||||
import { metaInfoKey } from './symbols'
|
||||
import { active } from './info/globals'
|
||||
import { Manager } from './manager'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentInternalInstance {
|
||||
$metaManager: Manager
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMetaPlugin(app: App, manager: Manager) {
|
||||
app.component('Metainfo', Metainfo)
|
||||
|
||||
app.config.globalProperties.$metaManager = manager
|
||||
app.provide(metaInfoKey, active)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { App } from 'vue'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { defaultMapping } from './config'
|
||||
import { applyMetaPlugin } from './install'
|
||||
import * as deepestResolver from './resolvers/deepest'
|
||||
import { TODO, ActiveResolverObject, MetaContext, PathSegments } from './types'
|
||||
|
||||
export type ManagerOptions = {
|
||||
install(app: App): void
|
||||
}
|
||||
|
||||
export type Manager = {
|
||||
readonly config: TODO
|
||||
|
||||
resolver: ActiveResolverObject
|
||||
install(app: App): void
|
||||
}
|
||||
|
||||
export function createManager(options: TODO = {}): Manager {
|
||||
const { resolver = deepestResolver } = options
|
||||
|
||||
// TODO: validate resolver
|
||||
|
||||
const manager: Manager = {
|
||||
resolver: {
|
||||
setup(context: MetaContext) {
|
||||
if (!resolver || isFunction(resolver)) {
|
||||
return
|
||||
}
|
||||
|
||||
resolver.setup(context)
|
||||
},
|
||||
resolve(key: string, pathSegments: PathSegments, getShadow, getActive) {
|
||||
if (!resolver) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isFunction(resolver)) {
|
||||
return resolver(key, pathSegments, getShadow, getActive)
|
||||
}
|
||||
|
||||
return resolver.resolve(key, pathSegments, getShadow, getActive)
|
||||
},
|
||||
},
|
||||
config: {
|
||||
...defaultMapping,
|
||||
...options.config,
|
||||
},
|
||||
install(app: App) {
|
||||
applyMetaPlugin(app, this)
|
||||
},
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { isObject } from '@vue/shared'
|
||||
import { update } from './info/update'
|
||||
import { MetaContext, MetainfoInput, PathSegments } from './types'
|
||||
|
||||
interface Target extends MetainfoInput {
|
||||
__vm_proxy?: any
|
||||
}
|
||||
|
||||
export function createProxy(
|
||||
target: Target,
|
||||
handler: ProxyHandler<object>
|
||||
): Target {
|
||||
return markRaw(new Proxy(target, handler))
|
||||
}
|
||||
|
||||
export function createHandler(
|
||||
context: MetaContext,
|
||||
pathSegments: PathSegments = []
|
||||
): ProxyHandler<object> {
|
||||
return {
|
||||
get(target: object, key: string, receiver: object) {
|
||||
const value = Reflect.get(target, key, receiver)
|
||||
|
||||
if (!isObject(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!value.__vm_proxy) {
|
||||
const keyPath: PathSegments = [...pathSegments, key]
|
||||
|
||||
const handler = /*#__PURE__*/ createHandler(context, keyPath)
|
||||
value.__vm_proxy = createProxy(value, handler)
|
||||
}
|
||||
|
||||
return value.__vm_proxy
|
||||
},
|
||||
set(
|
||||
target: object,
|
||||
key: string,
|
||||
value: unknown,
|
||||
receiver: object
|
||||
): boolean {
|
||||
update(context, pathSegments, key, value)
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import { h, VNode } from 'vue'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { getConfigKey } from './config'
|
||||
import { TODO } from './types'
|
||||
|
||||
export interface RenderContext {
|
||||
slots: any
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface GroupConfig {
|
||||
group: string
|
||||
data: Array<TODO> | TODO
|
||||
tagNamespace?: string
|
||||
fullName?: string
|
||||
slotName?: string
|
||||
}
|
||||
|
||||
export interface SlotScopeProperties {
|
||||
content: any
|
||||
metainfo: any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type RenderedMetainfoNode = {
|
||||
vnode: VNode
|
||||
target?: string
|
||||
}
|
||||
|
||||
export type RenderedMetainfo = Array<RenderedMetainfoNode>
|
||||
|
||||
export function renderMeta(
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
// console.info('renderMeta', key, data, config)
|
||||
|
||||
if (config.group) {
|
||||
return renderGroup(context, key, data, config)
|
||||
}
|
||||
|
||||
return renderTag(context, key, data, config)
|
||||
}
|
||||
|
||||
export function renderGroup(
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
// console.info('renderGroup', key, data, config)
|
||||
|
||||
if (isArray(data)) {
|
||||
config.contentAttributes = getConfigKey(
|
||||
[key, config.tag],
|
||||
'contentAttributes',
|
||||
config
|
||||
)
|
||||
return data.map(_data => renderTag(context, key, _data, config)).flat()
|
||||
}
|
||||
|
||||
return Object.keys(data)
|
||||
.map(childKey => {
|
||||
const groupConfig: GroupConfig = {
|
||||
group: key,
|
||||
data,
|
||||
}
|
||||
|
||||
if (config.namespaced) {
|
||||
groupConfig.tagNamespace =
|
||||
config.namespaced === true ? key : config.namespaced
|
||||
} else if (config.namespacedAttribute) {
|
||||
const namespace =
|
||||
config.namespacedAttribute === true ? key : config.namespacedAttribute
|
||||
|
||||
groupConfig.fullName = `${namespace}:${childKey}`
|
||||
groupConfig.slotName = `${namespace}(${childKey})`
|
||||
}
|
||||
|
||||
return renderTag(context, key, data[childKey], config, groupConfig)
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
export function renderTag(
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO = {},
|
||||
groupConfig?: GroupConfig
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
if (!config.group && isArray(data)) {
|
||||
data = { content: data }
|
||||
}
|
||||
|
||||
let content, hasChilds
|
||||
|
||||
if (isArray(data)) {
|
||||
return data
|
||||
.map(child => {
|
||||
return renderTag(context, key, child, config, groupConfig)
|
||||
})
|
||||
.flat()
|
||||
} else if (data.content && isArray(data.content)) {
|
||||
content = data.content.map((child: string | TODO) => {
|
||||
if (typeof child === 'string') {
|
||||
return child
|
||||
}
|
||||
return renderTag(context, key, child, config, groupConfig)
|
||||
})
|
||||
hasChilds = true
|
||||
} else {
|
||||
content = data
|
||||
}
|
||||
|
||||
const { tag = config.tag || key } = data
|
||||
|
||||
const fullName = (groupConfig && groupConfig.fullName) || key
|
||||
const slotName = (groupConfig && groupConfig.slotName) || key
|
||||
|
||||
let { attrs: attributes } = data
|
||||
if (!attributes && typeof data === 'object') {
|
||||
attributes = { ...data }
|
||||
|
||||
delete attributes.tag
|
||||
delete attributes.content
|
||||
delete attributes.target
|
||||
} else {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
if (hasChilds) {
|
||||
content = getSlotContent(context, slotName, content, config, data)
|
||||
} else {
|
||||
const contentAttributes = getConfigKey(tag, 'contentAttributes', config)
|
||||
|
||||
if (contentAttributes) {
|
||||
if (!config.nameless) {
|
||||
const nameAttribute = getConfigKey(tag, 'nameAttribute', config)
|
||||
if (nameAttribute) {
|
||||
attributes[nameAttribute] = fullName
|
||||
}
|
||||
}
|
||||
|
||||
const contentAttribute = config.contentAttribute || contentAttributes[0]
|
||||
attributes[contentAttribute] = getSlotContent(
|
||||
context,
|
||||
slotName,
|
||||
attributes[contentAttribute] || content,
|
||||
config,
|
||||
groupConfig
|
||||
)
|
||||
content = undefined
|
||||
} else {
|
||||
content = getSlotContent(context, slotName, content, config, data)
|
||||
}
|
||||
}
|
||||
|
||||
const finalTag =
|
||||
groupConfig && groupConfig.tagNamespace
|
||||
? `${groupConfig.tagNamespace}:${tag}`
|
||||
: tag
|
||||
|
||||
// console.info('FINAL TAG', finalTag)
|
||||
// console.log(' ATTRIBUTES', attributes)
|
||||
// console.log(' CONTENT', content)
|
||||
// // console.log(data, attributes, config)
|
||||
|
||||
if (hasChilds) {
|
||||
for (const child of content) {
|
||||
if (typeof child === 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (child.type === finalTag) {
|
||||
return content
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const vnode = h(finalTag, attributes, content)
|
||||
|
||||
return {
|
||||
target: data.target,
|
||||
vnode,
|
||||
}
|
||||
}
|
||||
|
||||
export function getSlotContent(
|
||||
{ metainfo, slots }: RenderContext,
|
||||
slotName: string,
|
||||
content: any,
|
||||
config: TODO,
|
||||
groupConfig?: GroupConfig
|
||||
): TODO {
|
||||
if (!slots[slotName]) {
|
||||
return content
|
||||
}
|
||||
|
||||
const slotProps: SlotScopeProperties = {
|
||||
content,
|
||||
metainfo,
|
||||
}
|
||||
|
||||
if (groupConfig && groupConfig.group) {
|
||||
slotProps[groupConfig.group] = groupConfig.data
|
||||
}
|
||||
|
||||
content = slots[slotName](slotProps)
|
||||
|
||||
if (content.length) {
|
||||
return content[0].children
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ActiveNode,
|
||||
/*ActiveResolverSetup, ActiveResolverMethod,*/ MetaContext,
|
||||
PathSegments,
|
||||
ShadowNode,
|
||||
} from '../types'
|
||||
|
||||
export function setup(context: MetaContext): void {}
|
||||
|
||||
export function resolve(
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadow: ShadowNode,
|
||||
active: ActiveNode
|
||||
): any {}
|
||||
@@ -0,0 +1 @@
|
||||
export interface Resolver {}
|
||||
@@ -1,12 +0,0 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import { serverSequences } from '../shared/escaping'
|
||||
import { setOptions } from '../shared/options'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
export default function generate (rawInfo, options) {
|
||||
options = setOptions(options)
|
||||
const metaInfo = getMetaInfo(options, rawInfo, serverSequences)
|
||||
|
||||
const serverInjector = generateServerInjector(options, metaInfo)
|
||||
return serverInjector.injectors
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys, defaultInfo } from '../shared/constants'
|
||||
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
|
||||
|
||||
/**
|
||||
* 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, metaInfo) {
|
||||
const serverInjector = {
|
||||
data: metaInfo,
|
||||
extraData: undefined,
|
||||
addInfo (appId, metaInfo) {
|
||||
this.extraData = this.extraData || {}
|
||||
this.extraData[appId] = metaInfo
|
||||
},
|
||||
callInjectors (opts) {
|
||||
const m = this.injectors
|
||||
|
||||
// only call title for the head
|
||||
return (opts.body || opts.pbody ? '' : m.title.text(opts)) +
|
||||
m.meta.text(opts) +
|
||||
m.link.text(opts) +
|
||||
m.style.text(opts) +
|
||||
m.script.text(opts) +
|
||||
m.noscript.text(opts)
|
||||
},
|
||||
injectors: {
|
||||
head: ln => serverInjector.callInjectors({ ln }),
|
||||
bodyPrepend: ln => serverInjector.callInjectors({ ln, pbody: true }),
|
||||
bodyAppend: ln => serverInjector.callInjectors({ ln, body: true })
|
||||
}
|
||||
}
|
||||
|
||||
for (const type in defaultInfo) {
|
||||
if (metaInfoOptionKeys.includes(type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
serverInjector.injectors[type] = {
|
||||
text (arg) {
|
||||
if (type === 'title') {
|
||||
return titleGenerator(options, type, serverInjector.data[type], arg)
|
||||
}
|
||||
|
||||
if (metaInfoAttributeKeys.includes(type)) {
|
||||
const attributeData = {}
|
||||
|
||||
const data = serverInjector.data[type]
|
||||
if (data) {
|
||||
for (const attr in data) {
|
||||
attributeData[attr] = {
|
||||
[options.ssrAppId]: data[attr]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serverInjector.extraData) {
|
||||
for (const appId in serverInjector.extraData) {
|
||||
const data = serverInjector.extraData[appId][type]
|
||||
if (data) {
|
||||
for (const attr in data) {
|
||||
attributeData[attr] = {
|
||||
...attributeData[attr],
|
||||
[appId]: data[attr]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributeGenerator(options, type, attributeData, arg)
|
||||
}
|
||||
|
||||
let str = tagGenerator(options, type, serverInjector.data[type], arg)
|
||||
|
||||
if (serverInjector.extraData) {
|
||||
for (const appId in serverInjector.extraData) {
|
||||
const data = serverInjector.extraData[appId][type]
|
||||
const extraStr = tagGenerator(options, type, data, { appId, ...arg })
|
||||
str = `${str}${extraStr}`
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return serverInjector
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { booleanHtmlAttributes } from '../../shared/constants'
|
||||
|
||||
/**
|
||||
* 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 (options, type, data, addSrrAttribute) {
|
||||
const { attribute, ssrAttribute } = options || {}
|
||||
let attributeStr = ''
|
||||
|
||||
for (const attr in data) {
|
||||
const attrData = data[attr]
|
||||
const attrValues = []
|
||||
|
||||
for (const appId in attrData) {
|
||||
attrValues.push(...[].concat(attrData[appId]))
|
||||
}
|
||||
|
||||
if (attrValues.length) {
|
||||
attributeStr += booleanHtmlAttributes.includes(attr) && attrValues.some(Boolean)
|
||||
? `${attr}`
|
||||
: `${attr}="${attrValues.join(' ')}"`
|
||||
|
||||
attributeStr += ' '
|
||||
}
|
||||
}
|
||||
|
||||
if (attributeStr) {
|
||||
attributeStr += `${attribute}="${encodeURI(JSON.stringify(data))}"`
|
||||
}
|
||||
|
||||
if (type === 'htmlAttrs' && addSrrAttribute) {
|
||||
return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}`
|
||||
}
|
||||
|
||||
return attributeStr
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as attributeGenerator } from './attribute'
|
||||
export { default as titleGenerator } from './title'
|
||||
export { default as tagGenerator } from './tag'
|
||||
@@ -1,92 +0,0 @@
|
||||
import {
|
||||
booleanHtmlAttributes,
|
||||
tagsWithoutEndTag,
|
||||
tagsWithInnerContent,
|
||||
tagAttributeAsInnerContent,
|
||||
tagProperties,
|
||||
commonDataAttributes
|
||||
} from '../../shared/constants'
|
||||
|
||||
/**
|
||||
* 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 (options, type, tags, generatorOptions) {
|
||||
const { ssrAppId, attribute, tagIDKeyName } = options || {}
|
||||
const { appId, body = false, pbody = false, ln = false } = generatorOptions || {}
|
||||
|
||||
const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
|
||||
|
||||
if (!tags || !tags.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// build a string containing all tags of this type
|
||||
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
|
||||
}
|
||||
|
||||
if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) {
|
||||
return tagsStr
|
||||
}
|
||||
|
||||
let attrs = tag.once ? '' : ` ${attribute}="${appId || 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) || tagProperties.includes(attr)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr === 'callback') {
|
||||
attrs += ' onload="this.__vm_l=1"'
|
||||
continue
|
||||
}
|
||||
|
||||
// these form the attribute list for this tag
|
||||
let prefix = ''
|
||||
if (dataAttributes.includes(attr)) {
|
||||
prefix = 'data-'
|
||||
}
|
||||
|
||||
const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr)
|
||||
if (isBooleanAttr && !tag[attr]) {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
const hasEndTag = !tagsWithoutEndTag.includes(type)
|
||||
|
||||
// these tag types will have content inserted
|
||||
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
|
||||
|
||||
// the final string for this specific tag
|
||||
return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` +
|
||||
(hasContent ? `${content}</${type}>` : '') +
|
||||
(ln ? '\n' : '')
|
||||
}, '')
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 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 (options, type, data, generatorOptions) {
|
||||
const { ln } = generatorOptions || {}
|
||||
|
||||
if (!data) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `<${type}>${data}</${type}>${ln ? '\n' : ''}`
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { serverSequences } from '../shared/escaping'
|
||||
import { rootConfigKey } from '../shared/constants'
|
||||
import { showWarningNotSupported } from '../shared/log'
|
||||
import { getComponentMetaInfo } from '../shared/getComponentOption'
|
||||
import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app'
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
/**
|
||||
* Converts the state of the meta info object such that each item
|
||||
* can be compiled to a tag string on the server
|
||||
*
|
||||
* @vm {Object} - Vue instance - ideally the root component
|
||||
* @return {Object} - server meta info with `toString` methods
|
||||
*/
|
||||
export default function inject (rootVm, options) {
|
||||
// make sure vue-meta was initiated
|
||||
if (!rootVm[rootConfigKey]) {
|
||||
showWarningNotSupported()
|
||||
return {}
|
||||
}
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
const rawInfo = getComponentMetaInfo(options, rootVm)
|
||||
|
||||
const metaInfo = getMetaInfo(options, rawInfo, serverSequences, rootVm)
|
||||
|
||||
// generate server injector
|
||||
const serverInjector = generateServerInjector(options, metaInfo)
|
||||
|
||||
// add meta info from additional apps
|
||||
const appsMetaInfo = getAppsMetaInfo()
|
||||
if (appsMetaInfo) {
|
||||
for (const additionalAppId in appsMetaInfo) {
|
||||
serverInjector.addInfo(additionalAppId, appsMetaInfo[additionalAppId])
|
||||
delete appsMetaInfo[additionalAppId]
|
||||
}
|
||||
clearAppsMetaInfo(true)
|
||||
}
|
||||
|
||||
return serverInjector.injectors
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import refresh from '../client/refresh'
|
||||
import inject from '../server/inject'
|
||||
import { addApp } from './additional-app'
|
||||
import { showWarningNotSupportedInBrowserBundle } from './log'
|
||||
import { addNavGuards } from './nav-guards'
|
||||
import { pause, resume } from './pausing'
|
||||
import { getOptions } from './options'
|
||||
|
||||
export default function $meta (options) {
|
||||
options = options || {}
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
* @return {Object} - injector
|
||||
*/
|
||||
const $root = this.$root
|
||||
|
||||
return {
|
||||
getOptions: () => getOptions(options),
|
||||
setOptions: (newOptions) => {
|
||||
const refreshNavKey = 'refreshOnceOnNavigation'
|
||||
if (newOptions && newOptions[refreshNavKey]) {
|
||||
options.refreshOnceOnNavigation = !!newOptions[refreshNavKey]
|
||||
addNavGuards($root)
|
||||
}
|
||||
|
||||
const debounceWaitKey = 'debounceWait'
|
||||
if (newOptions && debounceWaitKey in newOptions) {
|
||||
const debounceWait = parseInt(newOptions[debounceWaitKey])
|
||||
if (!isNaN(debounceWait)) {
|
||||
options.debounceWait = debounceWait
|
||||
}
|
||||
}
|
||||
|
||||
const waitOnDestroyedKey = 'waitOnDestroyed'
|
||||
if (newOptions && waitOnDestroyedKey in newOptions) {
|
||||
options.waitOnDestroyed = !!newOptions[waitOnDestroyedKey]
|
||||
}
|
||||
},
|
||||
refresh: () => refresh($root, options),
|
||||
inject: () => process.server ? inject($root, options) : showWarningNotSupportedInBrowserBundle('inject'),
|
||||
pause: () => pause($root),
|
||||
resume: () => resume($root),
|
||||
addApp: appId => addApp($root, appId, options)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import updateClientMetaInfo from '../client/updateClientMetaInfo'
|
||||
import { updateAttribute } from '../client/updaters'
|
||||
import { metaInfoAttributeKeys } from '../shared/constants'
|
||||
import { getTag, removeElementsByAppId } from '../utils/elements'
|
||||
|
||||
let appsMetaInfo
|
||||
|
||||
export function addApp (rootVm, appId, options) {
|
||||
return {
|
||||
set: metaInfo => setMetaInfo(rootVm, appId, options, metaInfo),
|
||||
remove: () => removeMetaInfo(rootVm, appId, options)
|
||||
}
|
||||
}
|
||||
|
||||
export function setMetaInfo (rootVm, appId, options, metaInfo) {
|
||||
// if a vm exists _and_ its mounted then immediately update
|
||||
if (rootVm && rootVm.$el) {
|
||||
return updateClientMetaInfo(appId, options, metaInfo)
|
||||
}
|
||||
|
||||
// store for later, the info
|
||||
// will be set on the first refresh
|
||||
appsMetaInfo = appsMetaInfo || {}
|
||||
appsMetaInfo[appId] = metaInfo
|
||||
}
|
||||
|
||||
export function removeMetaInfo (rootVm, appId, options) {
|
||||
if (rootVm && rootVm.$el) {
|
||||
const tags = {}
|
||||
for (const type of metaInfoAttributeKeys) {
|
||||
const tagName = type.substr(0, 4)
|
||||
updateAttribute(appId, options, type, {}, getTag(tags, tagName))
|
||||
}
|
||||
|
||||
return removeElementsByAppId(options, appId)
|
||||
}
|
||||
|
||||
if (appsMetaInfo[appId]) {
|
||||
delete appsMetaInfo[appId]
|
||||
clearAppsMetaInfo()
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppsMetaInfo () {
|
||||
return appsMetaInfo
|
||||
}
|
||||
|
||||
export function clearAppsMetaInfo (force) {
|
||||
if (force || !Object.keys(appsMetaInfo).length) {
|
||||
appsMetaInfo = undefined
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* These are constant variables used throughout the application.
|
||||
*/
|
||||
|
||||
// set some sane defaults
|
||||
export const defaultInfo = {
|
||||
title: undefined,
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
bodyAttrs: {},
|
||||
headAttrs: {},
|
||||
base: [],
|
||||
link: [],
|
||||
meta: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
}
|
||||
|
||||
export const rootConfigKey = '_vueMeta'
|
||||
|
||||
// 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.
|
||||
export const keyName = 'metaInfo'
|
||||
|
||||
// This is the attribute vue-meta arguments on elements to know which it should
|
||||
// manage and which it should ignore.
|
||||
export 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.
|
||||
export 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.
|
||||
export const tagIDKeyName = 'vmid'
|
||||
|
||||
// This is the key name for possible meta templates
|
||||
export const metaTemplateKeyName = 'template'
|
||||
|
||||
// This is the key name for the content-holding property
|
||||
export const contentKeyName = 'content'
|
||||
|
||||
// The id used for the ssr app
|
||||
export const ssrAppId = 'ssr'
|
||||
|
||||
// How long meta update
|
||||
export const debounceWait = 10
|
||||
|
||||
// How long meta update
|
||||
export const waitOnDestroyed = true
|
||||
|
||||
export const defaultOptions = {
|
||||
keyName,
|
||||
attribute,
|
||||
ssrAttribute,
|
||||
tagIDKeyName,
|
||||
contentKeyName,
|
||||
metaTemplateKeyName,
|
||||
waitOnDestroyed,
|
||||
debounceWait,
|
||||
ssrAppId
|
||||
}
|
||||
|
||||
// might be a bit ugly, but minimizes the browser bundles a bit
|
||||
const defaultInfoKeys = Object.keys(defaultInfo)
|
||||
|
||||
// The metaInfo property keys which are used to disable escaping
|
||||
export const disableOptionKeys = [
|
||||
defaultInfoKeys[12],
|
||||
defaultInfoKeys[13]
|
||||
]
|
||||
|
||||
// List of metaInfo property keys which are configuration options (and dont generate html)
|
||||
export const metaInfoOptionKeys = [
|
||||
defaultInfoKeys[1],
|
||||
defaultInfoKeys[2],
|
||||
'changed',
|
||||
...disableOptionKeys
|
||||
]
|
||||
|
||||
// List of metaInfo property keys which only generates attributes and no tags
|
||||
export const metaInfoAttributeKeys = [
|
||||
defaultInfoKeys[3],
|
||||
defaultInfoKeys[4],
|
||||
defaultInfoKeys[5]
|
||||
]
|
||||
|
||||
// HTML elements which support the onload event
|
||||
export 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
|
||||
export const tagsWithoutEndTag = ['base', 'meta', 'link']
|
||||
|
||||
// HTML elements which can have inner content (shortened to our needs)
|
||||
export const tagsWithInnerContent = ['noscript', 'script', 'style']
|
||||
|
||||
// Attributes which are inserted as childNodes instead of HTMLAttribute
|
||||
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json']
|
||||
|
||||
export const tagProperties = ['once', 'skip', 'template']
|
||||
|
||||
// Attributes which should be added with data- prefix
|
||||
export const commonDataAttributes = ['body', 'pbody']
|
||||
|
||||
// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202
|
||||
export const booleanHtmlAttributes = [
|
||||
'allowfullscreen',
|
||||
'amp',
|
||||
'amp-boilerplate',
|
||||
'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'
|
||||
]
|
||||
@@ -1,108 +0,0 @@
|
||||
import { isString, isArray, isPureObject } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { ensureIsArray } from '../utils/ensure'
|
||||
import { metaInfoOptionKeys, disableOptionKeys } from './constants'
|
||||
|
||||
export const serverSequences = [
|
||||
[/&/g, '&'],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, ''']
|
||||
]
|
||||
|
||||
export const clientSequences = [
|
||||
[/&/g, '\u0026'],
|
||||
[/</g, '\u003C'],
|
||||
[/>/g, '\u003E'],
|
||||
[/"/g, '\u0022'],
|
||||
[/'/g, '\u0027']
|
||||
]
|
||||
|
||||
// sanitizes potentially dangerous characters
|
||||
export function escape (info, options, escapeOptions, escapeKeys) {
|
||||
const { tagIDKeyName } = options
|
||||
const { doEscape = v => v } = escapeOptions
|
||||
const escaped = {}
|
||||
|
||||
for (const key in info) {
|
||||
const value = info[key]
|
||||
|
||||
// no need to escape configuration options
|
||||
if (includes(metaInfoOptionKeys, key)) {
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
// do not use destructuring for disableOptionKeys, it increases transpiled size
|
||||
// due to var checks while we are guaranteed the structure of the cb
|
||||
let disableKey = disableOptionKeys[0]
|
||||
|
||||
if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) {
|
||||
// this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
const tagId = info[tagIDKeyName]
|
||||
if (tagId) {
|
||||
disableKey = disableOptionKeys[1]
|
||||
|
||||
// keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped
|
||||
if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) {
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(value)) {
|
||||
escaped[key] = doEscape(value)
|
||||
} else if (isArray(value)) {
|
||||
escaped[key] = value.map((v) => {
|
||||
if (isPureObject(v)) {
|
||||
return escape(v, options, escapeOptions, true)
|
||||
}
|
||||
|
||||
return doEscape(v)
|
||||
})
|
||||
} else if (isPureObject(value)) {
|
||||
escaped[key] = escape(value, options, escapeOptions, true)
|
||||
} else {
|
||||
escaped[key] = value
|
||||
}
|
||||
|
||||
if (escapeKeys) {
|
||||
const escapedKey = doEscape(key)
|
||||
if (key !== escapedKey) {
|
||||
escaped[escapedKey] = escaped[key]
|
||||
delete escaped[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return escaped
|
||||
}
|
||||
|
||||
export function escapeMetaInfo (options, info, escapeSequences) {
|
||||
escapeSequences = escapeSequences || []
|
||||
// do not use destructuring for seq, it increases transpiled size
|
||||
// due to var checks while we are guaranteed the structure of the cb
|
||||
const escapeOptions = {
|
||||
doEscape: value => escapeSequences.reduce((val, seq) => val.replace(seq[0], seq[1]), value)
|
||||
}
|
||||
|
||||
disableOptionKeys.forEach((disableKey, index) => {
|
||||
if (index === 0) {
|
||||
ensureIsArray(info, disableKey)
|
||||
} else if (index === 1) {
|
||||
for (const key in info[disableKey]) {
|
||||
ensureIsArray(info[disableKey], key)
|
||||
}
|
||||
}
|
||||
|
||||
escapeOptions[disableKey] = info[disableKey]
|
||||
})
|
||||
|
||||
// begin sanitization
|
||||
return escape(info, options, escapeOptions)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { isObject } from '../utils/is-type'
|
||||
import { defaultInfo } from './constants'
|
||||
import { merge } from './merge'
|
||||
import { inMetaInfoBranch } from './meta-helpers'
|
||||
|
||||
export function getComponentMetaInfo (options, component) {
|
||||
return getComponentOption(options || {}, component, defaultInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `opts.option` $option value of the given `opts.component`.
|
||||
* If methods are encountered, they will be bound to the component context.
|
||||
* If `opts.deep` is true, will recursively merge all child component
|
||||
* `opts.option` $option values into the returned result.
|
||||
*
|
||||
* @param {Object} opts - options
|
||||
* @param {Object} opts.component - Vue component to fetch option data from
|
||||
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
||||
* @param {String} opts.keyName - the name of the option to look for
|
||||
* @param {Object} [result={}] - result so far
|
||||
* @return {Object} result - final aggregated result
|
||||
*/
|
||||
export function getComponentOption (options, component, result) {
|
||||
result = result || {}
|
||||
|
||||
if (component._inactive) {
|
||||
return result
|
||||
}
|
||||
|
||||
options = options || {}
|
||||
const { keyName } = options
|
||||
const { $metaInfo, $options, $children } = component
|
||||
|
||||
// only collect option data if it exists
|
||||
if ($options[keyName]) {
|
||||
// if $metaInfo exists then [keyName] was defined as a function
|
||||
// and set to the computed prop $metaInfo in the mixin
|
||||
// using the computed prop should be a small performance increase
|
||||
// because Vue caches those internally
|
||||
const data = $metaInfo || $options[keyName]
|
||||
|
||||
// only merge data with result when its an object
|
||||
// eg it could be a function when metaInfo() returns undefined
|
||||
// dueo to the or statement above
|
||||
if (isObject(data)) {
|
||||
result = merge(result, data, options)
|
||||
}
|
||||
}
|
||||
|
||||
// collect & aggregate child options if deep = true
|
||||
if ($children.length) {
|
||||
$children.forEach((childComponent) => {
|
||||
// check if the childComponent is in a branch
|
||||
// return otherwise so we dont walk all component branches unnecessarily
|
||||
if (!inMetaInfoBranch(childComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
result = getComponentOption(options, childComponent, result)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { findIndex } from '../utils/array'
|
||||
import { escapeMetaInfo } from '../shared/escaping'
|
||||
import { applyTemplate } from './template'
|
||||
|
||||
/**
|
||||
* 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 (options, info, escapeSequences, component) {
|
||||
options = options || {}
|
||||
escapeSequences = escapeSequences || []
|
||||
|
||||
const { tagIDKeyName } = options
|
||||
// Remove all "template" tags from meta
|
||||
|
||||
// backup the title chunk in case user wants access to it
|
||||
if (info.title) {
|
||||
info.titleChunk = info.title
|
||||
}
|
||||
|
||||
// replace title with populated template
|
||||
if (info.titleTemplate && info.titleTemplate !== '%s') {
|
||||
applyTemplate({ component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || '')
|
||||
}
|
||||
|
||||
// convert base tag to an array so it can be handled the same way
|
||||
// as the other tags
|
||||
if (info.base) {
|
||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||
}
|
||||
|
||||
if (info.meta) {
|
||||
// remove meta items with duplicate vmid's
|
||||
info.meta = info.meta.filter((metaItem, index, arr) => {
|
||||
const hasVmid = !!metaItem[tagIDKeyName]
|
||||
if (!hasVmid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isFirstItemForVmid = index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName])
|
||||
return isFirstItemForVmid
|
||||
})
|
||||
|
||||
// apply templates if needed
|
||||
info.meta.forEach(metaObject => applyTemplate(options, metaObject))
|
||||
}
|
||||
|
||||
return escapeMetaInfo(options, info, escapeSequences)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { hasGlobalWindow } from '../utils/window'
|
||||
|
||||
const _global = hasGlobalWindow ? window : global
|
||||
|
||||
const console = _global.console || {}
|
||||
|
||||
export function warn (str) {
|
||||
/* istanbul ignore next */
|
||||
if (!console || !console.warn) {
|
||||
return
|
||||
}
|
||||
|
||||
console.warn(str)
|
||||
}
|
||||
|
||||
export const showWarningNotSupportedInBrowserBundle = method => warn(`${method} is not supported in browser builds`)
|
||||
|
||||
export const showWarningNotSupported = () => warn('This vue app/component has no vue-meta configuration')
|
||||
@@ -1,111 +0,0 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import { includes, findIndex } from '../utils/array'
|
||||
import { applyTemplate } from './template'
|
||||
import { metaInfoAttributeKeys, booleanHtmlAttributes } from './constants'
|
||||
import { warn } from './log'
|
||||
|
||||
export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, contentKeyName }, target, source) {
|
||||
// we concat the arrays without merging objects contained in,
|
||||
// but we check for a `vmid` property on each object in the array
|
||||
// using an O(1) lookup associative array exploit
|
||||
const destination = []
|
||||
|
||||
if (!target.length && !source.length) {
|
||||
return destination
|
||||
}
|
||||
|
||||
target.forEach((targetItem, targetIndex) => {
|
||||
// no tagID so no need to check for duplicity
|
||||
if (!targetItem[tagIDKeyName]) {
|
||||
destination.push(targetItem)
|
||||
return
|
||||
}
|
||||
|
||||
const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName])
|
||||
const sourceItem = source[sourceIndex]
|
||||
|
||||
// source doesnt contain any duplicate vmid's, we can keep targetItem
|
||||
if (sourceIndex === -1) {
|
||||
destination.push(targetItem)
|
||||
return
|
||||
}
|
||||
|
||||
// when sourceItem explictly defines contentKeyName or innerHTML as undefined, its
|
||||
// an indication that we need to skip the default behaviour or child has preference over parent
|
||||
// which means we keep the targetItem and ignore/remove the sourceItem
|
||||
if (
|
||||
(contentKeyName in sourceItem && sourceItem[contentKeyName] === undefined) ||
|
||||
('innerHTML' in sourceItem && sourceItem.innerHTML === undefined)
|
||||
) {
|
||||
destination.push(targetItem)
|
||||
// remove current index from source array so its not concatenated to destination below
|
||||
source.splice(sourceIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem
|
||||
|
||||
// if source specifies null as content then ignore both the target as the source
|
||||
if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) {
|
||||
// remove current index from source array so its not concatenated to destination below
|
||||
source.splice(sourceIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// now we only need to check if the target has a template to combine it with the source
|
||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||
if (!targetTemplate) {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||
if (!sourceTemplate) {
|
||||
// use parent template and child content
|
||||
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, targetTemplate)
|
||||
|
||||
// set template to true to indicate template was already applied
|
||||
sourceItem.template = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!sourceItem[contentKeyName]) {
|
||||
// use parent content and child template
|
||||
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, undefined, targetItem[contentKeyName])
|
||||
}
|
||||
})
|
||||
|
||||
return destination.concat(source)
|
||||
}
|
||||
|
||||
let warningShown = false
|
||||
|
||||
export function merge (target, source, options) {
|
||||
options = options || {}
|
||||
|
||||
// remove properties explicitly set to false so child components can
|
||||
// optionally _not_ overwrite the parents content
|
||||
// (for array properties this is checked in arrayMerge)
|
||||
if (source.title === undefined) {
|
||||
delete source.title
|
||||
}
|
||||
|
||||
metaInfoAttributeKeys.forEach((attrKey) => {
|
||||
if (!source[attrKey]) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const key in source[attrKey]) {
|
||||
if (key in source[attrKey] && source[attrKey][key] === undefined) {
|
||||
if (includes(booleanHtmlAttributes, key) && !warningShown) {
|
||||
warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details')
|
||||
warningShown = true
|
||||
}
|
||||
delete source[attrKey][key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return deepmerge(target, source, {
|
||||
arrayMerge: (t, s) => arrayMerge(options, t, s)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { isUndefined, isObject } from '../utils/is-type'
|
||||
import { rootConfigKey } from './constants'
|
||||
|
||||
// Vue $root instance has a _vueMeta object property, otherwise its a boolean true
|
||||
export function hasMetaInfo (vm) {
|
||||
vm = vm || this
|
||||
return vm && (vm[rootConfigKey] === true || isObject(vm[rootConfigKey]))
|
||||
}
|
||||
|
||||
// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has
|
||||
export function inMetaInfoBranch (vm) {
|
||||
vm = vm || this
|
||||
return vm && !isUndefined(vm[rootConfigKey])
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import { triggerUpdate } from '../client/update'
|
||||
import { isUndefined, isFunction } from '../utils/is-type'
|
||||
import { find } from '../utils/array'
|
||||
import { rootConfigKey } from './constants'
|
||||
import { hasMetaInfo } from './meta-helpers'
|
||||
import { addNavGuards } from './nav-guards'
|
||||
import { warn } from './log'
|
||||
|
||||
let appId = 1
|
||||
|
||||
export default function createMixin (Vue, options) {
|
||||
// for which Vue lifecycle hooks should the metaInfo be refreshed
|
||||
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
|
||||
|
||||
// watch for client side component updates
|
||||
return {
|
||||
beforeCreate () {
|
||||
const rootKey = '$root'
|
||||
const $root = this[rootKey]
|
||||
const $options = this.$options
|
||||
const devtoolsEnabled = Vue.config.devtools
|
||||
|
||||
Object.defineProperty(this, '_hasMetaInfo', {
|
||||
configurable: true,
|
||||
get () {
|
||||
// Show deprecation warning once when devtools enabled
|
||||
if (devtoolsEnabled && !$root[rootConfigKey].deprecationWarningShown) {
|
||||
warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead')
|
||||
$root[rootConfigKey].deprecationWarningShown = true
|
||||
}
|
||||
return hasMetaInfo(this)
|
||||
}
|
||||
})
|
||||
|
||||
// Add a marker to know if it uses metaInfo
|
||||
// _vnode is used to know that it's attached to a real component
|
||||
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
||||
if (isUndefined($options[options.keyName]) || $options[options.keyName] === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$root[rootConfigKey]) {
|
||||
$root[rootConfigKey] = { appId }
|
||||
appId++
|
||||
|
||||
if (devtoolsEnabled && $root.$options[options.keyName]) {
|
||||
// use nextTick so the children should be added to $root
|
||||
this.$nextTick(() => {
|
||||
// find the first child that lists fnOptions
|
||||
const child = find($root.$children, c => c.$vnode && c.$vnode.fnOptions)
|
||||
if (child && child.$vnode.fnOptions[options.keyName]) {
|
||||
warn(`VueMeta has detected a possible global mixin which adds a ${options.keyName} property to all Vue components on the page. This could cause severe performance issues. If possible, use $meta().addApp to add meta information instead`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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[rootConfigKey]) {
|
||||
this[rootConfigKey] = true
|
||||
|
||||
let parent = this.$parent
|
||||
while (parent && parent !== $root) {
|
||||
if (isUndefined(parent[rootConfigKey])) {
|
||||
parent[rootConfigKey] = false
|
||||
}
|
||||
parent = parent.$parent
|
||||
}
|
||||
}
|
||||
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (isFunction($options[options.keyName])) {
|
||||
$options.computed = $options.computed || {}
|
||||
$options.computed.$metaInfo = $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.$on('hook:created', function () {
|
||||
this.$watch('$metaInfo', function () {
|
||||
triggerUpdate(options, this[rootKey], 'watcher')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// force an initial refresh on page load and prevent other lifecycleHooks
|
||||
// to triggerUpdate until this initial refresh is finished
|
||||
// this is to make sure that when a page is opened in an inactive tab which
|
||||
// has throttled rAF/timers we still immediately set the page title
|
||||
if (isUndefined($root[rootConfigKey].initialized)) {
|
||||
$root[rootConfigKey].initialized = this.$isServer
|
||||
|
||||
if (!$root[rootConfigKey].initialized) {
|
||||
if (!$root[rootConfigKey].initializedSsr) {
|
||||
$root[rootConfigKey].initializedSsr = true
|
||||
|
||||
this.$on('hook:beforeMount', function () {
|
||||
const $root = this
|
||||
// if this Vue-app was server rendered, set the appId to 'ssr'
|
||||
// only one SSR app per page is supported
|
||||
if ($root.$el && $root.$el.nodeType === 1 && $root.$el.hasAttribute('data-server-rendered')) {
|
||||
$root[rootConfigKey].appId = options.ssrAppId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// we use the mounted hook here as on page load
|
||||
this.$on('hook:mounted', function () {
|
||||
const $root = this[rootKey]
|
||||
|
||||
if (!$root[rootConfigKey].initialized) {
|
||||
// used in triggerUpdate to check if a change was triggered
|
||||
// during initialization
|
||||
$root[rootConfigKey].initializing = true
|
||||
|
||||
// refresh meta in nextTick so all child components have loaded
|
||||
this.$nextTick(function () {
|
||||
const { tags, metaInfo } = $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 && $root[rootConfigKey].initialized === null) {
|
||||
this.$nextTick(() => triggerUpdate(options, $root, 'init'))
|
||||
}
|
||||
|
||||
$root[rootConfigKey].initialized = true
|
||||
delete $root[rootConfigKey].initializing
|
||||
|
||||
// add the navigation guards if they havent been added yet
|
||||
// they are needed for the afterNavigation callback
|
||||
if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) {
|
||||
addNavGuards($root)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// add the navigation guards if requested
|
||||
if (options.refreshOnceOnNavigation) {
|
||||
addNavGuards($root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.$on('hook:destroyed', function () {
|
||||
// do not trigger refresh:
|
||||
// - when user configured to not wait for transitions on destroyed
|
||||
// - when the component doesnt have a parent
|
||||
// - doesnt have metaInfo defined
|
||||
if (!this.$parent || !hasMetaInfo(this)) {
|
||||
return
|
||||
}
|
||||
delete this._hasMetaInfo
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (!options.waitOnDestroyed || !this.$el || !this.$el.offsetParent) {
|
||||
triggerUpdate(options, this.$root, 'destroyed')
|
||||
return
|
||||
}
|
||||
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
/* istanbul ignore next line */
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
|
||||
triggerUpdate(options, this.$root, 'destroyed')
|
||||
}, 50)
|
||||
})
|
||||
})
|
||||
|
||||
// do not trigger refresh on the server side
|
||||
if (this.$isServer) {
|
||||
/* istanbul ignore next */
|
||||
return
|
||||
}
|
||||
|
||||
// no need to add this hooks on server side
|
||||
updateOnLifecycleHook.forEach((lifecycleHook) => {
|
||||
this.$on(`hook:${lifecycleHook}`, function () {
|
||||
triggerUpdate(options, this[rootKey], lifecycleHook)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { isFunction } from '../utils/is-type'
|
||||
import { rootConfigKey } from './constants'
|
||||
import { pause, resume } from './pausing'
|
||||
|
||||
export function addNavGuards (rootVm) {
|
||||
const router = rootVm.$router
|
||||
|
||||
// return when nav guards already added or no router exists
|
||||
if (rootVm[rootConfigKey].navGuards || !router) {
|
||||
/* istanbul ignore next */
|
||||
return
|
||||
}
|
||||
|
||||
rootVm[rootConfigKey].navGuards = true
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
pause(rootVm)
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
rootVm.$nextTick(() => {
|
||||
const { metaInfo } = resume(rootVm)
|
||||
|
||||
if (metaInfo && isFunction(metaInfo.afterNavigation)) {
|
||||
metaInfo.afterNavigation(metaInfo)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { isObject, isUndefined } from '../utils/is-type'
|
||||
import { defaultOptions } from './constants'
|
||||
|
||||
export function setOptions (options) {
|
||||
// combine options
|
||||
options = isObject(options) ? options : {}
|
||||
|
||||
// The options are set like this so they can
|
||||
// be minified by terser while keeping the
|
||||
// user api intact
|
||||
// terser --mangle-properties keep_quoted=strict
|
||||
/* eslint-disable dot-notation */
|
||||
return {
|
||||
keyName: options['keyName'] || defaultOptions.keyName,
|
||||
attribute: options['attribute'] || defaultOptions.attribute,
|
||||
ssrAttribute: options['ssrAttribute'] || defaultOptions.ssrAttribute,
|
||||
tagIDKeyName: options['tagIDKeyName'] || defaultOptions.tagIDKeyName,
|
||||
contentKeyName: options['contentKeyName'] || defaultOptions.contentKeyName,
|
||||
metaTemplateKeyName: options['metaTemplateKeyName'] || defaultOptions.metaTemplateKeyName,
|
||||
debounceWait: isUndefined(options['debounceWait']) ? defaultOptions.debounceWait : options['debounceWait'],
|
||||
waitOnDestroyed: isUndefined(options['waitOnDestroyed']) ? defaultOptions.waitOnDestroyed : options['waitOnDestroyed'],
|
||||
ssrAppId: options['ssrAppId'] || defaultOptions.ssrAppId,
|
||||
refreshOnceOnNavigation: !!options['refreshOnceOnNavigation']
|
||||
}
|
||||
/* eslint-enable dot-notation */
|
||||
}
|
||||
|
||||
export function getOptions (options) {
|
||||
const optionsCopy = {}
|
||||
for (const key in options) {
|
||||
optionsCopy[key] = options[key]
|
||||
}
|
||||
return optionsCopy
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { rootConfigKey } from './constants'
|
||||
|
||||
export function pause (rootVm, refresh) {
|
||||
rootVm[rootConfigKey].pausing = true
|
||||
|
||||
return () => resume(rootVm, refresh)
|
||||
}
|
||||
|
||||
export function resume (rootVm, refresh) {
|
||||
rootVm[rootConfigKey].pausing = false
|
||||
|
||||
if (refresh || refresh === undefined) {
|
||||
return rootVm.$meta().refresh()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { isUndefined, isFunction } from '../utils/is-type'
|
||||
|
||||
export function applyTemplate ({ component, metaTemplateKeyName, contentKeyName }, headObject, template, chunk) {
|
||||
if (template === true || headObject[metaTemplateKeyName] === true) {
|
||||
// abort, template was already applied
|
||||
return false
|
||||
}
|
||||
|
||||
if (isUndefined(template) && headObject[metaTemplateKeyName]) {
|
||||
template = headObject[metaTemplateKeyName]
|
||||
headObject[metaTemplateKeyName] = true
|
||||
}
|
||||
|
||||
// return early if no template defined
|
||||
if (!template) {
|
||||
// cleanup faulty template properties
|
||||
delete headObject[metaTemplateKeyName]
|
||||
return false
|
||||
}
|
||||
|
||||
if (isUndefined(chunk)) {
|
||||
chunk = headObject[contentKeyName]
|
||||
}
|
||||
|
||||
headObject[contentKeyName] = isFunction(template)
|
||||
? template.call(component, chunk)
|
||||
: template.replace(/%s/g, chunk)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import { MetainfoActive } from './types'
|
||||
|
||||
export const hasSymbol =
|
||||
typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'
|
||||
|
||||
export const PolySymbol = (name: string) =>
|
||||
// vm = vue meta
|
||||
hasSymbol
|
||||
? Symbol(__DEV__ ? '[vue-meta]: ' + name : name)
|
||||
: (__DEV__ ? '[vue-meta]: ' : '_vm_') + name
|
||||
|
||||
export const metaInfoKey = PolySymbol(
|
||||
__DEV__ ? 'metainfo' : 'mi'
|
||||
) as InjectionKey<MetainfoActive>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ComponentInternalInstance } from 'vue'
|
||||
import { Manager } from '../manager'
|
||||
|
||||
export type Immutable<T> = {
|
||||
readonly [P in keyof T]: Immutable<T[P]>
|
||||
}
|
||||
|
||||
export type TODO = any
|
||||
export type PathSegments = Array<string>
|
||||
|
||||
export interface MetainfoInput {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface MetainfoActive {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export type MetaContext = {
|
||||
id: string | symbol
|
||||
vm?: ComponentInternalInstance | null
|
||||
manager: Manager
|
||||
}
|
||||
|
||||
export type ActiveResolverSetup = (context: MetaContext) => void
|
||||
export type ActiveResolverMethod = (
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadow: ShadowNode,
|
||||
active: ActiveNode
|
||||
) => any
|
||||
|
||||
export interface ActiveResolverObject {
|
||||
setup?: ActiveResolverSetup
|
||||
resolve: ActiveResolverMethod
|
||||
}
|
||||
|
||||
export interface ShadowNode {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface ActiveNode {
|
||||
[key: string]: TODO
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { inject, getCurrentInstance, onUnmounted } from 'vue'
|
||||
import { setByObject, remove } from './info'
|
||||
import { Manager } from './manager'
|
||||
import { createProxy, createHandler } from './proxy'
|
||||
import { metaInfoKey, PolySymbol } from './symbols'
|
||||
import { MetaContext, MetainfoActive, MetainfoInput } from './types'
|
||||
|
||||
let contextCounter: number = 0
|
||||
|
||||
export function useMeta(obj: MetainfoInput, manager?: Manager) {
|
||||
const vm = getCurrentInstance()
|
||||
if (vm) {
|
||||
console.log(vm)
|
||||
manager = vm.appContext.config.globalProperties.$metaManager
|
||||
}
|
||||
|
||||
if (!manager) {
|
||||
// oopsydoopsy
|
||||
throw new Error('No manager or current instance')
|
||||
return
|
||||
}
|
||||
|
||||
const context: MetaContext = {
|
||||
id: PolySymbol(`context ${contextCounter++}`),
|
||||
vm,
|
||||
manager,
|
||||
}
|
||||
|
||||
let unmount = <T extends Function = () => any>() => remove(context)
|
||||
if (vm) {
|
||||
onUnmounted(unmount)
|
||||
}
|
||||
|
||||
if (manager.resolver.setup) {
|
||||
manager.resolver.setup(context)
|
||||
}
|
||||
|
||||
setByObject(context, obj)
|
||||
|
||||
const handler = /*#__PURE__*/ createHandler(context)
|
||||
const meta = createProxy(obj, handler)
|
||||
|
||||
return {
|
||||
meta,
|
||||
unmount,
|
||||
}
|
||||
}
|
||||
|
||||
export function useMetainfo(): MetainfoActive {
|
||||
return inject(metaInfoKey)!
|
||||
}
|
||||
|
||||
export function getCurrentManager(): Manager {
|
||||
const vm = getCurrentInstance()!
|
||||
return vm.appContext.config.globalProperties.$metaManager
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* To reduce build size, this file provides simple polyfills without
|
||||
* overly excessive type checking and without modifying
|
||||
* the global Array.prototype
|
||||
* The polyfills are automatically removed in the commonjs build
|
||||
* Also, only files in client/ & shared/ should use these functions
|
||||
* files in server/ still use normal js function
|
||||
*/
|
||||
|
||||
// this const is replaced by rollup to true for umd builds
|
||||
// which means the polyfills are removed for other build formats
|
||||
const polyfill = process.env.NODE_ENV === 'test'
|
||||
|
||||
export function find (array, predicate, thisArg) {
|
||||
if (polyfill && !Array.prototype.find) {
|
||||
// idx needs to be a Number, for..in returns string
|
||||
for (let idx = 0; idx < array.length; idx++) {
|
||||
if (predicate.call(thisArg, array[idx], idx, array)) {
|
||||
return array[idx]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
return array.find(predicate, thisArg)
|
||||
}
|
||||
|
||||
export function findIndex (array, predicate, thisArg) {
|
||||
if (polyfill && !Array.prototype.findIndex) {
|
||||
// idx needs to be a Number, for..in returns string
|
||||
for (let idx = 0; idx < array.length; idx++) {
|
||||
if (predicate.call(thisArg, array[idx], idx, array)) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return array.findIndex(predicate, thisArg)
|
||||
}
|
||||
|
||||
export function toArray (arg) {
|
||||
if (polyfill && !Array.from) {
|
||||
return Array.prototype.slice.call(arg)
|
||||
}
|
||||
return Array.from(arg)
|
||||
}
|
||||
|
||||
export function includes (array, value) {
|
||||
if (polyfill && !Array.prototype.includes) {
|
||||
for (const idx in array) {
|
||||
if (array[idx] === value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return array.includes(value)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isArray, isObject } from '@vue/shared'
|
||||
// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
|
||||
|
||||
export function clone(v: any): any {
|
||||
if (isArray(v)) {
|
||||
return v.map(clone)
|
||||
}
|
||||
|
||||
if (isObject(v)) {
|
||||
const res: any = {}
|
||||
|
||||
for (const key in v) {
|
||||
if (key === 'context') {
|
||||
res[key] = v[key]
|
||||
} else {
|
||||
res[key] = clone(v[key])
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { toArray } from './array'
|
||||
|
||||
export const querySelector = (arg, el) => (el || document).querySelectorAll(arg)
|
||||
|
||||
export function getTag (tags, tag) {
|
||||
if (!tags[tag]) {
|
||||
tags[tag] = document.getElementsByTagName(tag)[0]
|
||||
}
|
||||
|
||||
return tags[tag]
|
||||
}
|
||||
|
||||
export function getElementsKey ({ body, pbody }) {
|
||||
return body
|
||||
? 'body'
|
||||
: (pbody ? 'pbody' : 'head')
|
||||
}
|
||||
|
||||
export function queryElements (parentNode, { appId, attribute, type, tagIDKeyName }, attributes) {
|
||||
attributes = 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(querySelector(queries.join(', '), parentNode))
|
||||
}
|
||||
|
||||
export function removeElementsByAppId ({ attribute }, appId) {
|
||||
toArray(querySelector(`[${attribute}="${appId}"]`)).map(el => el.remove())
|
||||
}
|
||||
|
||||
export function removeAttribute (el, attributeName) {
|
||||
el.removeAttribute(attributeName)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { isArray, isObject } from './is-type'
|
||||
|
||||
export function ensureIsArray (arg, key) {
|
||||
if (!key || !isObject(arg)) {
|
||||
return isArray(arg) ? arg : []
|
||||
}
|
||||
|
||||
if (!isArray(arg[key])) {
|
||||
arg[key] = []
|
||||
}
|
||||
return arg
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './clone'
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* checks if passed argument is an array
|
||||
* @param {any} arg - the object to check
|
||||
* @return {Boolean} - true if `arg` is an array
|
||||
*/
|
||||
export function isArray (arg) {
|
||||
return Array.isArray(arg)
|
||||
}
|
||||
|
||||
export function isUndefined (arg) {
|
||||
return typeof arg === 'undefined'
|
||||
}
|
||||
|
||||
export function isObject (arg) {
|
||||
return typeof arg === 'object'
|
||||
}
|
||||
|
||||
export function isPureObject (arg) {
|
||||
return typeof arg === 'object' && arg !== null
|
||||
}
|
||||
|
||||
export function isFunction (arg) {
|
||||
return typeof arg === 'function'
|
||||
}
|
||||
|
||||
export function isString (arg) {
|
||||
return typeof arg === 'string'
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { isUndefined } from './is-type'
|
||||
|
||||
export function hasGlobalWindowFn () {
|
||||
try {
|
||||
return !isUndefined(window)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const hasGlobalWindow = hasGlobalWindowFn()
|
||||
Reference in New Issue
Block a user