273 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| var wrapVueWebComponent = (function () {
 | |
| 'use strict';
 | |
| 
 | |
| 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;
 | |
| 
 | |
| }());
 |