Spaces:
Sleeping
Sleeping
var wrapVueWebComponent = (function () { | |
; | |
const camelizeRE = /-(\w)/g; | |
const camelize = str => { | |
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') | |
}; | |
const hyphenateRE = /\B([A-Z])/g; | |
const hyphenate = str => { | |
return str.replace(hyphenateRE, '-$1').toLowerCase() | |
}; | |
function getInitialProps (propsList) { | |
const res = {}; | |
propsList.forEach(key => { | |
res[key] = undefined; | |
}); | |
return res | |
} | |
function injectHook (options, key, hook) { | |
options[key] = [].concat(options[key] || []); | |
options[key].unshift(hook); | |
} | |
function callHooks (vm, hook) { | |
if (vm) { | |
const hooks = vm.$options[hook] || []; | |
hooks.forEach(hook => { | |
hook.call(vm); | |
}); | |
} | |
} | |
function createCustomEvent (name, args) { | |
return new CustomEvent(name, { | |
bubbles: false, | |
cancelable: false, | |
detail: args | |
}) | |
} | |
const isBoolean = val => /function Boolean/.test(String(val)); | |
const isNumber = val => /function Number/.test(String(val)); | |
function convertAttributeValue (value, name, { type } = {}) { | |
if (isBoolean(type)) { | |
if (value === 'true' || value === 'false') { | |
return value === 'true' | |
} | |
if (value === '' || value === name || value != null) { | |
return true | |
} | |
return value | |
} else if (isNumber(type)) { | |
const parsed = parseFloat(value, 10); | |
return isNaN(parsed) ? value : parsed | |
} else { | |
return value | |
} | |
} | |
function toVNodes (h, children) { | |
const res = []; | |
for (let i = 0, l = children.length; i < l; i++) { | |
res.push(toVNode(h, children[i])); | |
} | |
return res | |
} | |
function toVNode (h, node) { | |
if (node.nodeType === 3) { | |
return node.data.trim() ? node.data : null | |
} else if (node.nodeType === 1) { | |
const data = { | |
attrs: getAttributes(node), | |
domProps: { | |
innerHTML: node.innerHTML | |
} | |
}; | |
if (data.attrs.slot) { | |
data.slot = data.attrs.slot; | |
delete data.attrs.slot; | |
} | |
return h(node.tagName, data) | |
} else { | |
return null | |
} | |
} | |
function getAttributes (node) { | |
const res = {}; | |
for (let i = 0, l = node.attributes.length; i < l; i++) { | |
const attr = node.attributes[i]; | |
res[attr.nodeName] = attr.nodeValue; | |
} | |
return res | |
} | |
function wrap (Vue, Component) { | |
const isAsync = typeof Component === 'function' && !Component.cid; | |
let isInitialized = false; | |
let hyphenatedPropsList; | |
let camelizedPropsList; | |
let camelizedPropsMap; | |
function initialize (Component) { | |
if (isInitialized) return | |
const options = typeof Component === 'function' | |
? Component.options | |
: Component; | |
// extract props info | |
const propsList = Array.isArray(options.props) | |
? options.props | |
: Object.keys(options.props || {}); | |
hyphenatedPropsList = propsList.map(hyphenate); | |
camelizedPropsList = propsList.map(camelize); | |
const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {}; | |
camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { | |
map[key] = originalPropsAsObject[propsList[i]]; | |
return map | |
}, {}); | |
// proxy $emit to native DOM events | |
injectHook(options, 'beforeCreate', function () { | |
const emit = this.$emit; | |
this.$emit = (name, ...args) => { | |
this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args)); | |
return emit.call(this, name, ...args) | |
}; | |
}); | |
injectHook(options, 'created', function () { | |
// sync default props values to wrapper on created | |
camelizedPropsList.forEach(key => { | |
this.$root.props[key] = this[key]; | |
}); | |
}); | |
// proxy props as Element properties | |
camelizedPropsList.forEach(key => { | |
Object.defineProperty(CustomElement.prototype, key, { | |
get () { | |
return this._wrapper.props[key] | |
}, | |
set (newVal) { | |
this._wrapper.props[key] = newVal; | |
}, | |
enumerable: false, | |
configurable: true | |
}); | |
}); | |
isInitialized = true; | |
} | |
function syncAttribute (el, key) { | |
const camelized = camelize(key); | |
const value = el.hasAttribute(key) ? el.getAttribute(key) : undefined; | |
el._wrapper.props[camelized] = convertAttributeValue( | |
value, | |
key, | |
camelizedPropsMap[camelized] | |
); | |
} | |
class CustomElement extends HTMLElement { | |
constructor () { | |
const self = super(); | |
self.attachShadow({ mode: 'open' }); | |
const wrapper = self._wrapper = new Vue({ | |
name: 'shadow-root', | |
customElement: self, | |
shadowRoot: self.shadowRoot, | |
data () { | |
return { | |
props: {}, | |
slotChildren: [] | |
} | |
}, | |
render (h) { | |
return h(Component, { | |
ref: 'inner', | |
props: this.props | |
}, this.slotChildren) | |
} | |
}); | |
// Use MutationObserver to react to future attribute & slot content change | |
const observer = new MutationObserver(mutations => { | |
let hasChildrenChange = false; | |
for (let i = 0; i < mutations.length; i++) { | |
const m = mutations[i]; | |
if (isInitialized && m.type === 'attributes' && m.target === self) { | |
syncAttribute(self, m.attributeName); | |
} else { | |
hasChildrenChange = true; | |
} | |
} | |
if (hasChildrenChange) { | |
wrapper.slotChildren = Object.freeze(toVNodes( | |
wrapper.$createElement, | |
self.childNodes | |
)); | |
} | |
}); | |
observer.observe(self, { | |
childList: true, | |
subtree: true, | |
characterData: true, | |
attributes: true | |
}); | |
} | |
get vueComponent () { | |
return this._wrapper.$refs.inner | |
} | |
connectedCallback () { | |
const wrapper = this._wrapper; | |
if (!wrapper._isMounted) { | |
// initialize attributes | |
const syncInitialAttributes = () => { | |
wrapper.props = getInitialProps(camelizedPropsList); | |
hyphenatedPropsList.forEach(key => { | |
syncAttribute(this, key); | |
}); | |
}; | |
if (isInitialized) { | |
syncInitialAttributes(); | |
} else { | |
// async & unresolved | |
Component().then(resolved => { | |
if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { | |
resolved = resolved.default; | |
} | |
initialize(resolved); | |
syncInitialAttributes(); | |
}); | |
} | |
// initialize children | |
wrapper.slotChildren = Object.freeze(toVNodes( | |
wrapper.$createElement, | |
this.childNodes | |
)); | |
wrapper.$mount(); | |
this.shadowRoot.appendChild(wrapper.$el); | |
} else { | |
callHooks(this.vueComponent, 'activated'); | |
} | |
} | |
disconnectedCallback () { | |
callHooks(this.vueComponent, 'deactivated'); | |
} | |
} | |
if (!isAsync) { | |
initialize(Component); | |
} | |
return CustomElement | |
} | |
return wrap; | |
}()); | |