mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-14 21:52:24 +03:00
feat: make attributes part of the metainfo object
feat: support removing old attributes feat: better support content as either attribute or child
This commit is contained in:
@@ -86,8 +86,18 @@ const App = {
|
||||
]
|
||||
},
|
||||
body: 'body-script1.js',
|
||||
htmlAttrs: {
|
||||
amp: true,
|
||||
lang: ['en', 'nl']
|
||||
},
|
||||
bodyAttrs: {
|
||||
class: ['theme-dark']
|
||||
},
|
||||
script: [
|
||||
{ src: 'head-script1.js' },
|
||||
{ json: { '@context': 'http://schema.org', unsafe: '<p>hello</p>' } },
|
||||
{ content: 'window.a = "<br/>"; </script><script>alert(\'asdasd\');' },
|
||||
{ rawContent: 'window.b = "<br/>"; </script><script> alert(\'123321\');' },
|
||||
{ src: 'body-script2.js', to: 'body' },
|
||||
{ src: 'body-script3.js', to: '#put-it-here' }
|
||||
],
|
||||
@@ -113,6 +123,7 @@ const App = {
|
||||
})
|
||||
|
||||
setTimeout(() => (meta.title = 'My Updated Title'), 2000)
|
||||
setTimeout(() => (meta.htmlAttrs.amp = undefined), 2000)
|
||||
|
||||
const metadata = useMetainfo()
|
||||
|
||||
@@ -144,7 +155,7 @@ const App = {
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<metainfo :metainfo="metadata" :body-class="'theme-dark'" :html-lang="['en','nl']" html-amp>
|
||||
<metainfo>
|
||||
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
|
||||
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
|
||||
<template v-slot:og(title)="{ content, metainfo, og }">
|
||||
|
||||
+8
-38
@@ -1,7 +1,7 @@
|
||||
import { h, watchEffect, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
|
||||
import { h, defineComponent, Teleport, VNode, VNodeProps } from 'vue'
|
||||
import { isArray, isFunction } from '@vue/shared'
|
||||
import { renderMeta } from './render'
|
||||
import { getCurrentManager } from './useApi'
|
||||
import { useMetainfo, getCurrentManager } from './useApi'
|
||||
import { MetainfoActive } from './types'
|
||||
|
||||
export interface MetainfoProps {
|
||||
@@ -24,42 +24,8 @@ export function addVnode (teleports: any, to: string, vnode: VNode | Array<VNode
|
||||
export const MetainfoImpl = defineComponent({
|
||||
name: 'Metainfo',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
metainfo: {
|
||||
type: Object as PropType<MetainfoActive>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup ({ metainfo }, { attrs, slots }) {
|
||||
const tags: { [key: string]: Element } = {}
|
||||
|
||||
watchEffect(() => {
|
||||
const attributes = Object.keys(attrs)
|
||||
|
||||
for (const tagName of ['html', 'head', 'body']) {
|
||||
const tagAttrs = attributes.filter(attr => attr.startsWith(tagName + '-'))
|
||||
|
||||
if (!tagAttrs.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!tags[tagName]) {
|
||||
const foundTag = document.querySelector(tagName)
|
||||
|
||||
if (foundTag) {
|
||||
tags[tagName] = foundTag
|
||||
}
|
||||
}
|
||||
|
||||
const tag: Element = tags[tagName]
|
||||
|
||||
for (const tagAttr of tagAttrs) {
|
||||
const attr: string = tagAttr.slice(5)
|
||||
|
||||
tag.setAttribute(attr, `${attrs[tagAttr] || ''}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
setup (_, { slots }) {
|
||||
const metainfo = useMetainfo()
|
||||
|
||||
return () => {
|
||||
const teleports: any = {}
|
||||
@@ -76,6 +42,10 @@ export const MetainfoImpl = defineComponent({
|
||||
config
|
||||
)
|
||||
|
||||
if (!vnodes) {
|
||||
continue
|
||||
}
|
||||
|
||||
const defaultTo =
|
||||
(key !== 'base' && metainfo[key].to) || config.to || 'head'
|
||||
|
||||
|
||||
+11
-17
@@ -1,19 +1,6 @@
|
||||
export interface ConfigOption {
|
||||
tag?: string
|
||||
to?: string
|
||||
group?: boolean
|
||||
keyAttribute?: string
|
||||
valueAttribute?: string
|
||||
nameless?: boolean
|
||||
namespaced?: boolean
|
||||
namespacedAttribute?: boolean
|
||||
}
|
||||
import { Config } from '../types'
|
||||
|
||||
export interface Config {
|
||||
[key: string]: ConfigOption
|
||||
}
|
||||
|
||||
const defaultConfig: Config = {
|
||||
export const defaultConfig: Config = {
|
||||
body: {
|
||||
tag: 'script',
|
||||
to: 'body'
|
||||
@@ -39,7 +26,14 @@ const defaultConfig: Config = {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta'
|
||||
},
|
||||
htmlAttrs: {
|
||||
attributesFor: 'html'
|
||||
},
|
||||
headAttrs: {
|
||||
attributesFor: 'head'
|
||||
},
|
||||
bodyAttrs: {
|
||||
attributesFor: 'body'
|
||||
}
|
||||
}
|
||||
|
||||
export { defaultConfig }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isArray } from '@vue/shared'
|
||||
import { Config } from './default'
|
||||
import { Config } from '../types'
|
||||
import { tags } from './tags'
|
||||
|
||||
export function hasConfig (name: string, config: Config): boolean {
|
||||
return !!config[name] || !!tags[name]
|
||||
}
|
||||
|
||||
export function getConfigKey (
|
||||
export function getConfigByKey (
|
||||
tagOrName: string | Array<string>,
|
||||
key: string,
|
||||
config: Config
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
export interface TagConfig {
|
||||
keyAttribute?: string
|
||||
contentAsAttribute?: boolean | string
|
||||
attributes: boolean | Array<string>
|
||||
[key: string]: any
|
||||
}
|
||||
@@ -15,13 +16,16 @@ const tags: { [key: string]: TagConfig } = {
|
||||
attributes: false
|
||||
},
|
||||
base: {
|
||||
contentAsAttribute: true,
|
||||
attributes: ['href', 'target']
|
||||
},
|
||||
meta: {
|
||||
contentAsAttribute: true,
|
||||
keyAttribute: 'name',
|
||||
attributes: ['content', 'name', 'http-equiv', 'charset']
|
||||
},
|
||||
link: {
|
||||
contentAsAttribute: true,
|
||||
attributes: [
|
||||
'href',
|
||||
'crossorigin',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { defaultConfig } from './config'
|
||||
export { createManager } from './manager'
|
||||
export * from './useApi'
|
||||
export * from './types'
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export function resolveActive (
|
||||
let value
|
||||
|
||||
if (shadowParent[key].length > 1) {
|
||||
// Is this useful? Idea is to prevent the user from messing with these options by mistake
|
||||
// Is using freeze 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]))
|
||||
|
||||
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
import { App } from 'vue'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { Config } from './config'
|
||||
import { applyMetaPlugin } from './install'
|
||||
import * as deepestResolver from './resolvers/deepest'
|
||||
import { ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
|
||||
import { Config, ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
|
||||
|
||||
export type Manager = {
|
||||
readonly config: Config
|
||||
|
||||
+107
-31
@@ -1,8 +1,15 @@
|
||||
import { h, VNode } from 'vue'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { getConfigKey } from './config'
|
||||
import { isArray, isString } from '@vue/shared'
|
||||
import { getConfigByKey } from './config'
|
||||
import { TODO } from './types'
|
||||
|
||||
const cachedElements: {
|
||||
[key: string]: {
|
||||
el: Element,
|
||||
attrs: Array<string>,
|
||||
}
|
||||
} = {}
|
||||
|
||||
export interface RenderContext {
|
||||
slots: any
|
||||
[key: string]: TODO
|
||||
@@ -34,9 +41,13 @@ export function renderMeta (
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
): void | RenderedMetainfo | RenderedMetainfoNode {
|
||||
console.info('renderMeta', key, data, config)
|
||||
|
||||
if (config.attributesFor) {
|
||||
return renderAttributes(context, key, data, config)
|
||||
}
|
||||
|
||||
if (config.group) {
|
||||
return renderGroup(context, key, data, config)
|
||||
}
|
||||
@@ -92,12 +103,8 @@ export function renderTag (
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
console.info('renderTag', key, data, config, groupConfig)
|
||||
|
||||
/* TODO: not needed I think
|
||||
if (!config.group && isArray(data)) {
|
||||
data = { children: data }
|
||||
} */
|
||||
|
||||
let content, hasChilds
|
||||
const contentAttributes = ['content', 'json', 'rawContent']
|
||||
const getConfig = (key: string) => getConfigByKey([tag, config.tag], key, config)
|
||||
|
||||
if (isArray(data)) {
|
||||
return data
|
||||
@@ -105,7 +112,19 @@ export function renderTag (
|
||||
return renderTag(context, key, child, config, groupConfig)
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
const { tag = config.tag || key } = data
|
||||
|
||||
let content
|
||||
let hasChilds: boolean = false
|
||||
let isRaw: boolean = false
|
||||
|
||||
if (isString(data)) {
|
||||
content = data
|
||||
} else if (data.children && isArray(data.children)) {
|
||||
hasChilds = true
|
||||
|
||||
content = data.children.map((child: string | TODO) => {
|
||||
const data = renderTag(context, key, child, config, groupConfig)
|
||||
|
||||
@@ -115,12 +134,23 @@ export function renderTag (
|
||||
|
||||
return data.vnode
|
||||
})
|
||||
hasChilds = true
|
||||
} else {
|
||||
content = data
|
||||
}
|
||||
let i = 0
|
||||
for (const contentAttribute of contentAttributes) {
|
||||
if (!content && data[contentAttribute]) {
|
||||
if (i === 1) {
|
||||
content = JSON.stringify(data[contentAttribute])
|
||||
} else {
|
||||
content = data[contentAttribute]
|
||||
}
|
||||
|
||||
const { tag = config.tag || key } = data
|
||||
isRaw = i > 1
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
const fullName = (groupConfig && groupConfig.fullName) || key
|
||||
const slotName = (groupConfig && groupConfig.slotName) || key
|
||||
@@ -132,33 +162,44 @@ export function renderTag (
|
||||
delete attributes.tag
|
||||
delete attributes.children
|
||||
delete attributes.to
|
||||
} else {
|
||||
|
||||
// cleanup all content attributes
|
||||
for (const attr of contentAttributes) {
|
||||
delete attributes[attr]
|
||||
}
|
||||
} else if (!attributes) {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
if (hasChilds) {
|
||||
content = getSlotContent(context, slotName, content, data)
|
||||
} else {
|
||||
const tagAttributes = getConfigKey([tag, config.tag], 'attributes', config)
|
||||
console.log('tagAttributes', tagAttributes, config, tag)
|
||||
if (tagAttributes) {
|
||||
const contentAsAttribute = getConfig('contentAsAttribute')
|
||||
let valueAttribute = config.valueAttribute
|
||||
|
||||
if (!valueAttribute && contentAsAttribute) {
|
||||
const tagAttributes = getConfig('attributes')
|
||||
valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]
|
||||
}
|
||||
|
||||
if (!valueAttribute) {
|
||||
content = getSlotContent(context, slotName, content, data)
|
||||
} else {
|
||||
if (!config.nameless) {
|
||||
const keyAttribute = getConfigKey([tag, config.tag], 'keyAttribute', config)
|
||||
const keyAttribute = getConfig('keyAttribute')
|
||||
if (keyAttribute) {
|
||||
attributes[keyAttribute] = fullName
|
||||
}
|
||||
}
|
||||
|
||||
const valueAttribute = config.valueAttribute || tagAttributes[0]
|
||||
attributes[valueAttribute] = getSlotContent(
|
||||
context,
|
||||
slotName,
|
||||
attributes[valueAttribute] || content,
|
||||
groupConfig
|
||||
)
|
||||
|
||||
content = undefined
|
||||
} else {
|
||||
content = getSlotContent(context, slotName, content, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,25 +213,60 @@ export function renderTag (
|
||||
// console.log(' CONTENT', content)
|
||||
// // console.log(data, attributes, config)
|
||||
|
||||
if (hasChilds) {
|
||||
for (const child of content) {
|
||||
if (child.type === finalTag) {
|
||||
// TODO: what was this about again?!?!?!?!
|
||||
return content
|
||||
}
|
||||
let vnode
|
||||
|
||||
break
|
||||
}
|
||||
if (isRaw) {
|
||||
attributes.innerHTML = content
|
||||
vnode = h(finalTag, attributes)
|
||||
} else {
|
||||
vnode = h(finalTag, attributes, content)
|
||||
}
|
||||
|
||||
const vnode = h(finalTag, attributes, content)
|
||||
|
||||
return {
|
||||
to: data.to,
|
||||
vnode
|
||||
}
|
||||
}
|
||||
|
||||
export function renderAttributes (
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO = {}
|
||||
): void {
|
||||
console.info('renderAttributes', key, data, config)
|
||||
|
||||
const { attributesFor } = config
|
||||
|
||||
if (!cachedElements[attributesFor]) {
|
||||
const el = document.querySelector(attributesFor)
|
||||
|
||||
if (el) {
|
||||
cachedElements[attributesFor] = {
|
||||
el,
|
||||
attrs: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { el, attrs } = cachedElements[attributesFor]
|
||||
|
||||
for (const attr in data) {
|
||||
const content = getSlotContent(context, `${key}(${attr})`, data[attr], data)
|
||||
|
||||
el.setAttribute(attr, `${content || ''}`)
|
||||
|
||||
if (!attrs.includes(attr)) {
|
||||
attrs.push(attr)
|
||||
}
|
||||
}
|
||||
|
||||
const attrsToRemove = attrs.filter(attr => !data[attr])
|
||||
for (const attr of attrsToRemove) {
|
||||
el.removeAttribute(attr)
|
||||
}
|
||||
}
|
||||
|
||||
export function getSlotContent (
|
||||
{ metainfo, slots }: RenderContext,
|
||||
slotName: string,
|
||||
|
||||
@@ -8,6 +8,22 @@ export type Immutable<T> = {
|
||||
export type TODO = any
|
||||
export type PathSegments = Array<string>
|
||||
|
||||
export interface ConfigOption {
|
||||
tag?: string
|
||||
to?: string
|
||||
group?: boolean
|
||||
keyAttribute?: string
|
||||
valueAttribute?: string
|
||||
nameless?: boolean
|
||||
namespaced?: boolean
|
||||
namespacedAttribute?: boolean
|
||||
attributesFor?: string
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
[key: string]: ConfigOption
|
||||
}
|
||||
|
||||
export interface MetainfoInput {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user