quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
319 lines (273 loc) • 7.45 kB
JavaScript
import Vue from 'vue'
import { isSSR, fromSSR } from './Platform.js'
import extend from '../utils/extend.js'
let updateId, ssrTakeover
function normalize (meta) {
if (meta.title) {
meta.title = meta.titleTemplate
? meta.titleTemplate(meta.title)
: meta.title
delete meta.titleTemplate
}
;[['meta', 'content'], ['link', 'href']].forEach(type => {
const
metaType = meta[type[0]],
metaProp = type[1]
for (const name in metaType) {
const metaLink = metaType[name]
if (metaLink.template) {
if (Object.keys(metaLink).length === 1) {
delete metaType[name]
}
else {
metaLink[metaProp] = metaLink.template(metaLink[metaProp] || '')
delete metaLink.template
}
}
}
})
}
function changed (old, def) {
if (Object.keys(old).length !== Object.keys(def).length) {
return true
}
for (const key in old) {
if (old[key] !== def[key]) {
return true
}
}
}
function bodyFilter (name) {
return ['class', 'style'].includes(name) === false
}
function htmlFilter (name) {
return ['lang', 'dir'].includes(name) === false
}
function diff (meta, other) {
const add = {}, remove = {}
if (meta === void 0) {
return { add: other, remove }
}
if (meta.title !== other.title) {
add.title = other.title
}
;['meta', 'link', 'script', 'htmlAttr', 'bodyAttr'].forEach(type => {
const old = meta[type], cur = other[type]
remove[type] = []
if (old === void 0 || old === null) {
add[type] = cur
return
}
add[type] = {}
for (const key in old) {
if (cur.hasOwnProperty(key) === false) {
remove[type].push(key)
}
}
for (const key in cur) {
if (old.hasOwnProperty(key) === false) {
add[type][key] = cur[key]
}
else if (changed(old[key], cur[key]) === true) {
remove[type].push(key)
add[type][key] = cur[key]
}
}
})
return { add, remove }
}
function apply ({ add, remove }) {
if (add.title) {
document.title = add.title
}
if (Object.keys(remove).length > 0) {
['meta', 'link', 'script'].forEach(type => {
remove[type].forEach(name => {
document.head.querySelector(`${type}[data-qmeta="${name}"]`).remove()
})
})
remove.htmlAttr.filter(htmlFilter).forEach(name => {
document.documentElement.removeAttribute(name)
})
remove.bodyAttr.filter(bodyFilter).forEach(name => {
document.body.removeAttribute(name)
})
}
;['meta', 'link', 'script'].forEach(type => {
const metaType = add[type]
for (const name in metaType) {
const tag = document.createElement(type)
for (const att in metaType[name]) {
if (att !== 'innerHTML') {
tag.setAttribute(att, metaType[name][att])
}
}
tag.setAttribute('data-qmeta', name)
if (type === 'script') {
tag.innerHTML = metaType[name].innerHTML || ''
}
document.head.appendChild(tag)
}
})
Object.keys(add.htmlAttr).filter(htmlFilter).forEach(name => {
document.documentElement.setAttribute(name, add.htmlAttr[name] || '')
})
Object.keys(add.bodyAttr).filter(bodyFilter).forEach(name => {
document.body.setAttribute(name, add.bodyAttr[name] || '')
})
}
function parseMeta (component, meta) {
if (component._inactive === true) { return }
// if it has meta
if (hasMeta(component) === true) {
extend(true, meta, component.__qMeta)
if (component.$options.meta.stopPropagation === true) {
return
}
}
component.$children.forEach(child => {
parseMeta(child, meta)
})
}
function updateClient () {
if (ssrTakeover === true) {
ssrTakeover = false
this.$root.__currentMeta = window.__Q_META__
}
const meta = {
title: '',
titleTemplate: null,
meta: {},
link: {},
script: {},
htmlAttr: {},
bodyAttr: {}
}
parseMeta(this.$root, meta)
normalize(meta)
apply(diff(this.$root.__currentMeta, meta))
this.$root.__currentMeta = meta
}
function getAttr (seed) {
return att => {
const val = seed[att]
return att + (val !== void 0 ? `="${val}"` : '')
}
}
function getHead (meta) {
let output = ''
if (meta.title) {
output += `<title>${meta.title}</title>`
}
;['meta', 'link', 'script'].forEach(type => {
const metaType = meta[type]
for (const att in metaType) {
const attrs = Object.keys(metaType[att])
.filter(att => att !== 'innerHTML')
.map(getAttr(metaType[att]))
output += `<${type} ${attrs.join(' ')} data-qmeta="${att}">`
if (type === 'script') {
output += (metaType[att].innerHTML || '') + `</script>`
}
}
})
return output
}
function getServerMeta (app, html, ctx) {
const meta = {
title: '',
titleTemplate: null,
meta: {},
link: {},
htmlAttr: {},
bodyAttr: {},
noscript: {}
}
parseMeta(app, meta)
normalize(meta)
const nonce = ctx !== void 0 && ctx.nonce !== void 0
? ` nonce="${ctx.nonce}"`
: ''
const tokens = {
'%%Q_HTML_ATTRS%%': Object.keys(meta.htmlAttr)
.filter(htmlFilter)
.map(getAttr(meta.htmlAttr))
.join(' '),
'%%Q_HEAD_TAGS%%': getHead(meta),
'%%Q_BODY_ATTRS%%': Object.keys(meta.bodyAttr)
.filter(bodyFilter)
.map(getAttr(meta.bodyAttr))
.join(' '),
'%%Q_BODY_TAGS%%': Object.keys(meta.noscript)
.map(name => `<noscript data-qmeta="${name}">${meta.noscript[name]}</noscript>`)
.join('') +
`<script${nonce}>window.__Q_META__=${delete meta.noscript && JSON.stringify(meta)}</script>`
}
Object.keys(tokens).forEach(key => {
html = html.replace(key, tokens[key])
})
return html
}
function beforeCreate () {
if (typeof this.$options.meta === 'function') {
if (this.$options.computed === void 0) {
this.$options.computed = {}
}
this.$options.computed.__qMeta = this.$options.meta
}
else if (hasMeta(this) === true) {
this.__qMeta = this.$options.meta
}
}
// needs to be really fast
function hasMeta (vm) {
return vm.$options.meta !== void 0 &&
vm.$options.meta !== null
}
function triggerMeta () {
hasMeta(this) === true && this.__qMetaUpdate()
}
export default {
install ({ queues }) {
if (isSSR === true) {
Vue.prototype.$getMetaHTML = app => {
return (html, ctx) => getServerMeta(app, html, ctx)
}
Vue.mixin({ beforeCreate })
queues.server.push((_, ctx) => {
ctx.ssr.Q_HTML_ATTRS += ' %%Q_HTML_ATTRS%%'
Object.assign(ctx.ssr, {
Q_HEAD_TAGS: '%%Q_HEAD_TAGS%%',
Q_BODY_ATTRS: '%%Q_BODY_ATTRS%%',
Q_BODY_TAGS: '%%Q_BODY_TAGS%%'
})
})
}
else {
ssrTakeover = fromSSR
Vue.mixin({
beforeCreate,
created () {
if (hasMeta(this) === true) {
this.__qMetaUnwatch = this.$watch('__qMeta', this.__qMetaUpdate)
}
},
activated: triggerMeta,
deactivated: triggerMeta,
beforeMount: triggerMeta,
destroyed () {
if (hasMeta(this) === true) {
this.__qMetaUnwatch()
this.__qMetaUpdate()
}
},
methods: {
__qMetaUpdate () {
clearTimeout(updateId)
updateId = setTimeout(updateClient.bind(this), 50)
}
}
})
}
}
}