├── src ├── vueRootInfo.js ├── lazyReactInVue.js ├── lazyVueInReact.js ├── cleanStyle.js ├── applyReactRouterInVue.js ├── reactAllHandles.js ├── index.js ├── applyRedux.js ├── withVueRouter.js ├── applyVuex.js ├── options.js ├── portal-vue.esm.js ├── applyVueInReact.js └── applyReactInVue.js ├── .gitignore ├── .babelrc ├── types └── vuereact.d.ts ├── rollup.config.js ├── package.json └── README.md /src/vueRootInfo.js: -------------------------------------------------------------------------------- 1 | const vueRootInfo = {} 2 | export default vueRootInfo 3 | -------------------------------------------------------------------------------- /src/lazyReactInVue.js: -------------------------------------------------------------------------------- 1 | import applyReactInVue from './applyReactInVue' 2 | export default function lazyReactInVue (asyncImport, useReactOptions) { 3 | return () => asyncImport().then((mod) => { 4 | return applyReactInVue(mod.default, useReactOptions) 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /src/lazyVueInReact.js: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react' 2 | import applyVueInReact from './applyVueInReact' 3 | export default function lazyVueInReact (asyncImport, useVueOptions) { 4 | return lazy(() => asyncImport().then((mod) => { 5 | return { default: applyVueInReact(mod.default, useVueOptions) } 6 | })) 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": ["@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-class-properties"], 7 | "env": { 8 | "rollup": { 9 | "plugins": ["@babel/plugin-external-helpers"] 10 | }, 11 | "test": { 12 | "presets": ["es2015"], 13 | "plugins": ["@babel/plugin-proposal-class-properties"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cleanStyle.js: -------------------------------------------------------------------------------- 1 | const style = document.createElement('style') 2 | const styleValue = { 3 | position: 'static', 4 | display: 'inline', 5 | margin: 0, 6 | padding: 0, 7 | float: 'none', 8 | border: 0, 9 | 'line-height': 'normal', 10 | background: 'none', 11 | width: 'auto', 12 | height: 'auto' 13 | } 14 | const cssText = '[__use_react_component_wrap],[data-use-vue-component-wrap],[__use_react_slot_wrap]' + '{' + Object.keys(styleValue).map(function(v){return v + ':' + styleValue[v] + ';'}).join('') + '}' 15 | const head = document.getElementsByTagName('head')[0] 16 | style.type = 'text/css' 17 | try { 18 | style.appendChild(document.createTextNode(cssText)) 19 | } catch (e) { 20 | style.styleSheet.cssText = 'cssText' // IE 21 | } 22 | head && head.appendChild(style) 23 | -------------------------------------------------------------------------------- /src/applyReactRouterInVue.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | let reactRouterInfo = {} 3 | function applyReactRouterInVue (withRouter) { 4 | // 只允许调用一次 5 | if (reactRouterInfo.withRouter) return reactRouterInfo.withRouter 6 | reactRouterInfo.withRouter = withRouter 7 | return reactRouterInfo.withRouter 8 | } 9 | export { 10 | reactRouterInfo 11 | } 12 | export function setReactRouterInVue (reactRouter) { 13 | if (reactRouterInfo.vueInstance) { 14 | updateReactRouterInVue(reactRouter) 15 | return 16 | } 17 | reactRouterInfo.vueInstance = new Vue({ 18 | data: { 19 | ...reactRouter 20 | } 21 | }) 22 | Vue.prototype.$reactRouter = reactRouterInfo.vueInstance.$data 23 | } 24 | export function updateReactRouterInVue (reactRouter) { 25 | Object.assign(reactRouterInfo.vueInstance.$data, { ...reactRouter }) 26 | } 27 | export default applyReactRouterInVue 28 | -------------------------------------------------------------------------------- /src/reactAllHandles.js: -------------------------------------------------------------------------------- 1 | export default new Set(['onClick', 'onContextMenu', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp', 'onChange', 'onInput', 'onInvalid', 'onReset', 'onSubmit', 'onError', 'onLoad', 'onPointerDown', 'onPointerMove', 'onPointerUp', 'onPointerCancel', 'onGotPointerCapture', 'onLostPointerCapture', 'onPointerEnter', 'onPointerLeave', 'onPointerOver', 'onPointerOut', 'onSelect', 'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart', 'onScroll', 'onWheel', 'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded', 'onError', 'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange', 'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting', 'onLoad', 'onError', 'onAnimationStart', 'onAnimationEnd', 'onAnimationIteration', 'onTransitionEnd', 'onToggle']) 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import lazyVueInReact from './lazyVueInReact' 2 | import lazyReactInVue from './lazyReactInVue' 3 | import applyReactInVue from './applyReactInVue' 4 | import applyVueInReact, { VueContainer } from './applyVueInReact' 5 | import applyRedux from './applyRedux' 6 | import applyVuex, { connectVuex } from './applyVuex' 7 | import withVueRouter from './withVueRouter' 8 | import vueRootInfo from './vueRootInfo' 9 | import applyReactRouterInVue from './applyReactRouterInVue' 10 | import REACT_ALL_HANDLERS from './reactAllHandles' 11 | // import './cleanStyle' 12 | // 兼容旧的方法名(因为旧的方法名中的use与react hook有冲突) 13 | const useReactInVue = applyReactInVue 14 | const useVueInReact = applyVueInReact 15 | const useRedux = applyRedux 16 | const useVuex = applyVuex 17 | export { 18 | lazyVueInReact, 19 | lazyReactInVue, 20 | applyReactInVue, 21 | applyVueInReact, 22 | VueContainer, 23 | applyRedux, 24 | applyVuex, 25 | connectVuex, 26 | useReactInVue, 27 | useVueInReact, 28 | useRedux, 29 | useVuex, 30 | withVueRouter, 31 | vueRootInfo, 32 | applyReactRouterInVue, 33 | REACT_ALL_HANDLERS 34 | } 35 | -------------------------------------------------------------------------------- /types/vuereact.d.ts: -------------------------------------------------------------------------------- 1 | type VueComponent = any; 2 | type ReactComponent = any; 3 | type options = object | null | undefined; 4 | interface applyReduxOptions { 5 | store: any; 6 | ReactReduxContext: any; 7 | } 8 | interface vuexStore { 9 | mapStateToProps?: null | undefined | ((state: any) => object); 10 | mapGettersToProps?: null | undefined | ((getters: any) => object); 11 | mapCommitToProps?: null | undefined | ((commit: any) => any); 12 | mapDispatchToProps?: null | undefined | ( (dispatch: any) => any); 13 | } 14 | export const applyReactInVue: (ReactComponent: ReactComponent, options?: options) => VueComponent; 15 | export const applyVueInReact: (VueComponent: ReactComponent, options?: options) => ReactComponent; 16 | export const lazyVueInReact: (asyncImport: Promise, options?: options) => any; 17 | export const lazyReactInVue: (asyncImport: Promise, options?: options) => any; 18 | export const VueContainer: any; 19 | export const applyRedux: (applyReduxOptions: applyReduxOptions) => any; 20 | export const applyVuex: (vuexStore: any) => any; 21 | export const connectVuex: (vuexStore: vuexStore) => any; 22 | export const withVueRouter: (ReactComponent: ReactComponent) => ReactComponent; 23 | export const applyReactRouterInVue: (ReactRouterWithRouter: any) => any; 24 | -------------------------------------------------------------------------------- /src/applyRedux.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | class ReduxLinkVue { 3 | constructor (store) { 4 | if (!store || !store.subscribe || !store.getState) { 5 | throw Error('incorrect store passed in, please check the function applyRedux\'s parameter must contains redux store') 6 | return 7 | } 8 | this.store = store 9 | // 订阅 10 | // 由于vue组件的设计机制,store是中心化的存在,使得不用关心取消订阅 11 | store.subscribe(() => { 12 | this._vm.state = store.getState() 13 | }) 14 | // 利用一个vue实例做双向绑定 15 | this._vm = new Vue({ 16 | data () { 17 | return { 18 | state: store.getState() // 初始化的数据 19 | } 20 | } 21 | }) 22 | } 23 | // 访问state对象时候,就直接返回响应式的数据 24 | get state () { 25 | return this._vm.state 26 | } 27 | get dispatch () { 28 | return this.store.dispatch 29 | } 30 | } 31 | let reduxInstance 32 | function applyRedux ({ store, ReactReduxContext }) { 33 | // 只允许调用一次 34 | if (reduxInstance) return reduxInstance 35 | // 创建redux与vue组件的连接 36 | reduxInstance = new ReduxLinkVue(store) 37 | // 如果提供了ReactReduxContext,就注册到applyReactInVue中,允许在vue中使用的react组件可以使用redux 38 | if (ReactReduxContext) { 39 | reduxInstance.ReactReduxContext = ReactReduxContext 40 | } 41 | // 和react-redux的provider不同,这里直接设计所有vue组件都注入redux 42 | Vue.prototype.$redux = reduxInstance 43 | return reduxInstance 44 | } 45 | export default applyRedux 46 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-object-spread/prefer-object-spread */ 2 | 3 | import resolve from 'rollup-plugin-node-resolve' 4 | import babel from 'rollup-plugin-babel' 5 | import {uglify} from 'rollup-plugin-uglify' 6 | import commonjs from 'rollup-plugin-commonjs' 7 | 8 | const shared = { 9 | input: 'src/index.js', 10 | plugins: [ 11 | resolve({ 12 | customResolveOptions: { 13 | moduleDirectory: 'node_modules', 14 | }, 15 | }), 16 | babel({ 17 | exclude: 'node-modules/**', 18 | }), 19 | commonjs(), 20 | uglify({ 21 | compress: { 22 | // 这个设置会导致压缩时一些不该被删的代码被误删 23 | // pure_getters: true, 24 | // unsafe: true, 25 | // unsafe_comps: true 26 | } 27 | }) 28 | ], 29 | external: ['react', 'react-dom', 'vue', 'portal-vue'], 30 | } 31 | 32 | export default [ 33 | Object.assign({}, shared, { 34 | output: { 35 | file: 'dist/vuereact.umd.js', 36 | format: 'umd', 37 | name: 'vuereact', 38 | globals: { 39 | react: 'React', 40 | 'react-dom': 'ReactDOM', 41 | vue: 'Vue' 42 | }, 43 | }, 44 | }), 45 | Object.assign({}, shared, { 46 | output: { 47 | file: 'dist/vuereact.esm.js', 48 | format: 'esm', 49 | name: 'vuereact', 50 | globals: { 51 | react: 'React', 52 | 'react-dom': 'ReactDOM', 53 | vue: 'Vue' 54 | }, 55 | }, 56 | }), 57 | ] 58 | -------------------------------------------------------------------------------- /src/withVueRouter.js: -------------------------------------------------------------------------------- 1 | import vueRootInfo from './vueRootInfo' 2 | import React from 'react' 3 | import Vue from 'vue' 4 | const vueRouterInfo = {} 5 | export default function widthVueRouter (Component) { 6 | class WithVueRouterCom extends React.Component { 7 | constructor (props) { 8 | super(props) 9 | // 判断是否有vue的router 10 | if (!vueRouterInfo.instance) { 11 | if (!vueRootInfo.router) { 12 | throw Error('Vue router does not exist! You must setting the Vue router in the Vue Instance options first.') 13 | } 14 | // 这里需要借用一个vue实例来完成watch 15 | vueRouterInfo.instance = new Vue({ 16 | router: vueRootInfo.router 17 | }) 18 | } 19 | this.state = { 20 | $vueRouter: vueRouterInfo.instance.$router, 21 | $vueRoute: vueRouterInfo.instance.$route 22 | } 23 | } 24 | componentDidMount () { 25 | // 订阅 26 | this.subscribe = vueRouterInfo.instance.$watch('$route', () => { 27 | this.setState({ 28 | $vueRouter: vueRouterInfo.instance.$router, 29 | $vueRoute: vueRouterInfo.instance.$route 30 | }) 31 | }) 32 | } 33 | componentWillUnmount () { 34 | // 停止订阅 35 | this.subscribe() 36 | } 37 | render () { 38 | return ( 39 | 40 | ) 41 | } 42 | } 43 | // 转发ref 44 | return React.forwardRef((props, ref) => ( 45 | 46 | )) 47 | } 48 | -------------------------------------------------------------------------------- /src/applyVuex.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import vueRootInfo from './vueRootInfo' 3 | let vuexStore 4 | export function connectVuex ({ mapStateToProps = (state) => {}, mapGettersToProps = (getters) => {}, mapCommitToProps = (commit) => {}, mapDispatchToProps = (dispatch) => {} }) { 5 | return function (Component) { 6 | class VuexCom extends React.Component { 7 | constructor (props) { 8 | super(props) 9 | if (vueRootInfo.store) { 10 | vuexStore = vueRootInfo.store 11 | } 12 | if (!vuexStore || !vuexStore.state || !vuexStore.subscribe || !vuexStore.dispatch || !vuexStore.commit) { 13 | throw Error('[vuereact-combined warn]Error: incorrect store passed in, please check the function applyVuex\'s parameter must be vuex store') 14 | } 15 | this.state = {...mapStateToProps(vuexStore.state), ...mapGettersToProps(vuexStore.getters)} 16 | } 17 | componentDidMount () { 18 | // 订阅 19 | this.watch = vuexStore.watch(function() { 20 | return {...mapStateToProps(vuexStore.state), ...mapGettersToProps(vuexStore.getters)} 21 | }, (newVal) => { 22 | this.setState(newVal) 23 | }, { 24 | deep: true 25 | }) 26 | } 27 | componentWillUnmount () { 28 | // 停止订阅 29 | this.watch() 30 | } 31 | render () { 32 | return ( 33 | 34 | ) 35 | } 36 | } 37 | // 转发ref 38 | return React.forwardRef((props, ref) => ( 39 | 40 | )) 41 | } 42 | } 43 | 44 | export default function applyVuex (store) { 45 | vuexStore = store 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuereact-combined", 3 | "private": false, 4 | "version": "1.2.11", 5 | "description": "Vue和React快捷集成的工具包,并且适合复杂的集成场景", 6 | "main": "dist/vuereact.umd.js", 7 | "module": "dist/vuereact.esm.js", 8 | "typings": "types/vuereact.d.ts", 9 | "scripts": { 10 | "build": "cross-env BABEL_ENV=rollup rollup -c" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/devilwjp/vuereact-combined.git" 15 | }, 16 | "author": "Devle Wu", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/devilwjp/vuereact-combined/issues" 20 | }, 21 | "files": [ 22 | "dist", 23 | "types" 24 | ], 25 | "homepage": "https://github.com/devilwjp/vuereact-combined#readme", 26 | "peerDependencies": { 27 | "react": ">= 16.3.0", 28 | "react-dom": ">= 16.3.0", 29 | "vue": "^2.6.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.7.4", 33 | "@babel/plugin-external-helpers": "^7.7.4", 34 | "@babel/plugin-proposal-class-properties": "^7.7.4", 35 | "@babel/plugin-proposal-object-rest-spread": "^7.7.4", 36 | "@babel/preset-env": "^7.7.4", 37 | "@babel/preset-react": "^7.7.4", 38 | "babel-preset-latest": "^6.24.1", 39 | "cross-env": "^6.0.3", 40 | "react": "^16.12.0", 41 | "react-dom": "^16.12.0", 42 | "rollup": "^1.27.5", 43 | "rollup-plugin-babel": "^4.3.3", 44 | "rollup-plugin-commonjs": "^10.1.0", 45 | "rollup-plugin-node-resolve": "^5.2.0", 46 | "rollup-plugin-uglify": "^6.0.3", 47 | "vue": "^2.6.10", 48 | "vue-template-compiler": "^2.6.10", 49 | "vue-template-es2015-compiler": "^1.9.1" 50 | }, 51 | "keywords": [ 52 | "vue", 53 | "react", 54 | "vuereact", 55 | "reactvue", 56 | "vueinreact", 57 | "reactinvue", 58 | "redux", 59 | "vuex", 60 | "lazyvue", 61 | "lazyreact" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const originOptions = { 4 | react: { 5 | componentWrap: 'div', 6 | slotWrap: 'div', 7 | componentWrapAttrs: { 8 | __use_react_component_wrap: '', 9 | style: { 10 | all: 'unset' 11 | } 12 | }, 13 | slotWrapAttrs: { 14 | __use_react_slot_wrap: '', 15 | style: { 16 | all: 'unset' 17 | } 18 | } 19 | }, 20 | vue: { 21 | // 组件wrapper 22 | componentWrapHOC: (VueComponentMountAt, nativeProps = []) => { 23 | // 传入portals 24 | return function ({ portals = [] } = {}) { 25 | return (
{VueComponentMountAt}{portals.map(({ Portal, key }) => )}
) 26 | } 27 | }, 28 | componentWrapAttrs: { 29 | 'data-use-vue-component-wrap': '', 30 | style: { 31 | all: 'unset', 32 | } 33 | }, 34 | slotWrapAttrs: { 35 | 'data-use-vue-slot-wrap': '', 36 | style: { 37 | all: 'unset' 38 | } 39 | } 40 | } 41 | } 42 | 43 | export function setOptions (newOptions = { 44 | react: {}, 45 | vue: {} 46 | }, options = originOptions, clone) { 47 | if (!newOptions.vue) { 48 | newOptions.vue = {} 49 | } 50 | if (!newOptions.react) { 51 | newOptions.react = {} 52 | } 53 | const params = [options, { 54 | ...newOptions, 55 | react: { 56 | ...options.react, 57 | ...newOptions.react, 58 | componentWrapAttrs: { 59 | ...options.react.componentWrapAttrs, 60 | ...newOptions.react.componentWrapAttrs 61 | }, 62 | slotWrapAttrs: { 63 | ...options.react.slotWrapAttrs, 64 | ...newOptions.react.slotWrapAttrs 65 | } 66 | }, 67 | vue: { 68 | ...options.vue, 69 | ...newOptions.vue, 70 | componentWrapAttrs: { 71 | ...options.vue.componentWrapAttrs, 72 | ...newOptions.vue.componentWrapAttrs 73 | }, 74 | slotWrapAttrs: { 75 | ...options.vue.slotWrapAttrs, 76 | ...newOptions.vue.slotWrapAttrs 77 | } 78 | } 79 | }] 80 | if (clone) { 81 | params.unshift({}) 82 | } 83 | 84 | return Object.assign.apply(this, params) 85 | } 86 | 87 | export default originOptions 88 | -------------------------------------------------------------------------------- /src/portal-vue.esm.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * portal-vue © Thorsten Lünborg, 2019 4 | * 5 | * Version: 2.1.7 6 | * 7 | * LICENCE: MIT 8 | * 9 | * https://github.com/linusborg/portal-vue 10 | * 11 | */ 12 | 13 | import Vue from 'vue'; 14 | 15 | function _typeof(obj) { 16 | if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { 17 | _typeof = function (obj) { 18 | return typeof obj; 19 | }; 20 | } else { 21 | _typeof = function (obj) { 22 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 23 | }; 24 | } 25 | 26 | return _typeof(obj); 27 | } 28 | 29 | function _toConsumableArray(arr) { 30 | return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); 31 | } 32 | 33 | function _arrayWithoutHoles(arr) { 34 | if (Array.isArray(arr)) { 35 | for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 36 | 37 | return arr2; 38 | } 39 | } 40 | 41 | function _iterableToArray(iter) { 42 | if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); 43 | } 44 | 45 | function _nonIterableSpread() { 46 | throw new TypeError("Invalid attempt to spread non-iterable instance"); 47 | } 48 | 49 | var inBrowser = typeof window !== 'undefined'; 50 | function freeze(item) { 51 | if (Array.isArray(item) || _typeof(item) === 'object') { 52 | return Object.freeze(item); 53 | } 54 | 55 | return item; 56 | } 57 | function combinePassengers(transports) { 58 | var slotProps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 59 | return transports.reduce(function (passengers, transport) { 60 | var temp = transport.passengers[0]; 61 | var newPassengers = typeof temp === 'function' ? temp(slotProps) : transport.passengers; 62 | return passengers.concat(newPassengers); 63 | }, []); 64 | } 65 | function stableSort(array, compareFn) { 66 | return array.map(function (v, idx) { 67 | return [idx, v]; 68 | }).sort(function (a, b) { 69 | return compareFn(a[1], b[1]) || a[0] - b[0]; 70 | }).map(function (c) { 71 | return c[1]; 72 | }); 73 | } 74 | function pick(obj, keys) { 75 | return keys.reduce(function (acc, key) { 76 | if (obj.hasOwnProperty(key)) { 77 | acc[key] = obj[key]; 78 | } 79 | 80 | return acc; 81 | }, {}); 82 | } 83 | 84 | var transports = {}; 85 | var targets = {}; 86 | var sources = {}; 87 | var Wormhole = Vue.extend({ 88 | data: function data() { 89 | return { 90 | transports: transports, 91 | targets: targets, 92 | sources: sources, 93 | trackInstances: inBrowser 94 | }; 95 | }, 96 | methods: { 97 | open: function open(transport) { 98 | if (!inBrowser) return; 99 | var to = transport.to, 100 | from = transport.from, 101 | passengers = transport.passengers, 102 | _transport$order = transport.order, 103 | order = _transport$order === void 0 ? Infinity : _transport$order; 104 | if (!to || !from || !passengers) return; 105 | var newTransport = { 106 | to: to, 107 | from: from, 108 | passengers: freeze(passengers), 109 | order: order 110 | }; 111 | var keys = Object.keys(this.transports); 112 | 113 | if (keys.indexOf(to) === -1) { 114 | Vue.set(this.transports, to, []); 115 | } 116 | 117 | var currentIndex = this.$_getTransportIndex(newTransport); // Copying the array here so that the PortalTarget change event will actually contain two distinct arrays 118 | 119 | var newTransports = this.transports[to].slice(0); 120 | 121 | if (currentIndex === -1) { 122 | newTransports.push(newTransport); 123 | } else { 124 | newTransports[currentIndex] = newTransport; 125 | } 126 | 127 | this.transports[to] = stableSort(newTransports, function (a, b) { 128 | return a.order - b.order; 129 | }); 130 | }, 131 | close: function close(transport) { 132 | var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 133 | var to = transport.to, 134 | from = transport.from; 135 | if (!to || !from && force === false) return; 136 | 137 | if (!this.transports[to]) { 138 | return; 139 | } 140 | 141 | if (force) { 142 | this.transports[to] = []; 143 | } else { 144 | var index = this.$_getTransportIndex(transport); 145 | 146 | if (index >= 0) { 147 | // Copying the array here so that the PortalTarget change event will actually contain two distinct arrays 148 | var newTransports = this.transports[to].slice(0); 149 | newTransports.splice(index, 1); 150 | this.transports[to] = newTransports; 151 | } 152 | } 153 | }, 154 | registerTarget: function registerTarget(target, vm, force) { 155 | if (!inBrowser) return; 156 | 157 | if (this.trackInstances && !force && this.targets[target]) { 158 | console.warn("[portal-vue]: Target ".concat(target, " already exists")); 159 | } 160 | 161 | this.$set(this.targets, target, Object.freeze([vm])); 162 | }, 163 | unregisterTarget: function unregisterTarget(target) { 164 | this.$delete(this.targets, target); 165 | }, 166 | registerSource: function registerSource(source, vm, force) { 167 | if (!inBrowser) return; 168 | 169 | if (this.trackInstances && !force && this.sources[source]) { 170 | console.warn("[portal-vue]: source ".concat(source, " already exists")); 171 | } 172 | 173 | this.$set(this.sources, source, Object.freeze([vm])); 174 | }, 175 | unregisterSource: function unregisterSource(source) { 176 | this.$delete(this.sources, source); 177 | }, 178 | hasTarget: function hasTarget(to) { 179 | return !!(this.targets[to] && this.targets[to][0]); 180 | }, 181 | hasSource: function hasSource(to) { 182 | return !!(this.sources[to] && this.sources[to][0]); 183 | }, 184 | hasContentFor: function hasContentFor(to) { 185 | return !!this.transports[to] && !!this.transports[to].length; 186 | }, 187 | // Internal 188 | $_getTransportIndex: function $_getTransportIndex(_ref) { 189 | var to = _ref.to, 190 | from = _ref.from; 191 | 192 | for (var i in this.transports[to]) { 193 | if (this.transports[to][i].from === from) { 194 | return +i; 195 | } 196 | } 197 | 198 | return -1; 199 | } 200 | } 201 | }); 202 | var wormhole = new Wormhole(transports); 203 | 204 | var _id = 1; 205 | var Portal = Vue.extend({ 206 | name: 'portal', 207 | props: { 208 | disabled: { 209 | type: Boolean 210 | }, 211 | name: { 212 | type: String, 213 | default: function _default() { 214 | return String(_id++); 215 | } 216 | }, 217 | order: { 218 | type: Number, 219 | default: 0 220 | }, 221 | slim: { 222 | type: Boolean 223 | }, 224 | slotProps: { 225 | type: Object, 226 | default: function _default() { 227 | return {}; 228 | } 229 | }, 230 | tag: { 231 | type: String, 232 | default: 'DIV' 233 | }, 234 | to: { 235 | type: String, 236 | default: function _default() { 237 | return String(Math.round(Math.random() * 10000000)); 238 | } 239 | } 240 | }, 241 | created: function created() { 242 | var _this = this; 243 | 244 | this.$nextTick(function () { 245 | wormhole.registerSource(_this.name, _this); 246 | }); 247 | }, 248 | mounted: function mounted() { 249 | if (!this.disabled) { 250 | this.sendUpdate(); 251 | } 252 | }, 253 | updated: function updated() { 254 | if (this.disabled) { 255 | this.clear(); 256 | } else { 257 | this.sendUpdate(); 258 | } 259 | }, 260 | beforeDestroy: function beforeDestroy() { 261 | wormhole.unregisterSource(this.name); 262 | this.clear(); 263 | }, 264 | watch: { 265 | to: function to(newValue, oldValue) { 266 | oldValue && oldValue !== newValue && this.clear(oldValue); 267 | this.sendUpdate(); 268 | } 269 | }, 270 | methods: { 271 | clear: function clear(target) { 272 | var closer = { 273 | from: this.name, 274 | to: target || this.to 275 | }; 276 | wormhole.close(closer); 277 | }, 278 | normalizeSlots: function normalizeSlots() { 279 | return this.$scopedSlots.default ? [this.$scopedSlots.default] : this.$slots.default; 280 | }, 281 | normalizeOwnChildren: function normalizeOwnChildren(children) { 282 | return typeof children === 'function' ? children(this.slotProps) : children; 283 | }, 284 | sendUpdate: function sendUpdate() { 285 | var slotContent = this.normalizeSlots(); 286 | 287 | if (slotContent) { 288 | var transport = { 289 | from: this.name, 290 | to: this.to, 291 | passengers: _toConsumableArray(slotContent), 292 | order: this.order 293 | }; 294 | wormhole.open(transport); 295 | } else { 296 | this.clear(); 297 | } 298 | } 299 | }, 300 | render: function render(h) { 301 | var children = this.$slots.default || this.$scopedSlots.default || []; 302 | var Tag = this.tag; 303 | 304 | if (children && this.disabled) { 305 | return children.length <= 1 && this.slim ? this.normalizeOwnChildren(children)[0] : h(Tag, [this.normalizeOwnChildren(children)]); 306 | } else { 307 | return this.slim ? h() : h(Tag, { 308 | class: { 309 | 'v-portal': true 310 | }, 311 | style: { 312 | display: 'none' 313 | }, 314 | key: 'v-portal-placeholder' 315 | }); 316 | } 317 | } 318 | }); 319 | 320 | var PortalTarget = Vue.extend({ 321 | name: 'portalTarget', 322 | props: { 323 | multiple: { 324 | type: Boolean, 325 | default: false 326 | }, 327 | name: { 328 | type: String, 329 | required: true 330 | }, 331 | slim: { 332 | type: Boolean, 333 | default: false 334 | }, 335 | slotProps: { 336 | type: Object, 337 | default: function _default() { 338 | return {}; 339 | } 340 | }, 341 | tag: { 342 | type: String, 343 | default: 'div' 344 | }, 345 | transition: { 346 | type: [String, Object, Function] 347 | } 348 | }, 349 | data: function data() { 350 | return { 351 | transports: wormhole.transports, 352 | firstRender: true 353 | }; 354 | }, 355 | created: function created() { 356 | var _this = this; 357 | 358 | this.$nextTick(function () { 359 | wormhole.registerTarget(_this.name, _this); 360 | }); 361 | }, 362 | watch: { 363 | ownTransports: function ownTransports() { 364 | this.$emit('change', this.children().length > 0); 365 | }, 366 | name: function name(newVal, oldVal) { 367 | /** 368 | * TODO 369 | * This should warn as well ... 370 | */ 371 | wormhole.unregisterTarget(oldVal); 372 | wormhole.registerTarget(newVal, this); 373 | } 374 | }, 375 | mounted: function mounted() { 376 | var _this2 = this; 377 | 378 | if (this.transition) { 379 | this.$nextTick(function () { 380 | // only when we have a transition, because it causes a re-render 381 | _this2.firstRender = false; 382 | }); 383 | } 384 | }, 385 | beforeDestroy: function beforeDestroy() { 386 | wormhole.unregisterTarget(this.name); 387 | }, 388 | computed: { 389 | ownTransports: function ownTransports() { 390 | var transports = this.transports[this.name] || []; 391 | 392 | if (this.multiple) { 393 | return transports; 394 | } 395 | 396 | return transports.length === 0 ? [] : [transports[transports.length - 1]]; 397 | }, 398 | passengers: function passengers() { 399 | return combinePassengers(this.ownTransports, this.slotProps); 400 | } 401 | }, 402 | methods: { 403 | // can't be a computed prop because it has to "react" to $slot changes. 404 | children: function children() { 405 | return this.passengers.length !== 0 ? this.passengers : this.$scopedSlots.default ? this.$scopedSlots.default(this.slotProps) : this.$slots.default || []; 406 | }, 407 | // can't be a computed prop because it has to "react" to this.children(). 408 | noWrapper: function noWrapper() { 409 | var noWrapper = this.slim && !this.transition; 410 | 411 | if (noWrapper && this.children().length > 1) { 412 | console.warn('[portal-vue]: PortalTarget with `slim` option received more than one child element.'); 413 | } 414 | 415 | return noWrapper; 416 | } 417 | }, 418 | render: function render(h) { 419 | var noWrapper = this.noWrapper(); 420 | var children = this.children(); 421 | var Tag = this.transition || this.tag; 422 | return noWrapper ? children[0] : this.slim && !Tag ? h() : h(Tag, { 423 | props: { 424 | // if we have a transition component, pass the tag if it exists 425 | tag: this.transition && this.tag ? this.tag : undefined 426 | }, 427 | class: { 428 | 'vue-portal-target': true 429 | } 430 | }, children); 431 | } 432 | }); 433 | 434 | var _id$1 = 0; 435 | var portalProps = ['disabled', 'name', 'order', 'slim', 'slotProps', 'tag', 'to']; 436 | var targetProps = ['multiple', 'transition']; 437 | var MountingPortal = Vue.extend({ 438 | name: 'MountingPortal', 439 | inheritAttrs: false, 440 | props: { 441 | append: { 442 | type: [Boolean, String] 443 | }, 444 | bail: { 445 | type: Boolean 446 | }, 447 | mountTo: { 448 | type: String, 449 | required: true 450 | }, 451 | // Portal 452 | disabled: { 453 | type: Boolean 454 | }, 455 | // name for the portal 456 | name: { 457 | type: String, 458 | default: function _default() { 459 | return 'mounted_' + String(_id$1++); 460 | } 461 | }, 462 | order: { 463 | type: Number, 464 | default: 0 465 | }, 466 | slim: { 467 | type: Boolean 468 | }, 469 | slotProps: { 470 | type: Object, 471 | default: function _default() { 472 | return {}; 473 | } 474 | }, 475 | tag: { 476 | type: String, 477 | default: 'DIV' 478 | }, 479 | // name for the target 480 | to: { 481 | type: String, 482 | default: function _default() { 483 | return String(Math.round(Math.random() * 10000000)); 484 | } 485 | }, 486 | // Target 487 | multiple: { 488 | type: Boolean, 489 | default: false 490 | }, 491 | targetSlim: { 492 | type: Boolean 493 | }, 494 | targetSlotProps: { 495 | type: Object, 496 | default: function _default() { 497 | return {}; 498 | } 499 | }, 500 | targetTag: { 501 | type: String, 502 | default: 'div' 503 | }, 504 | transition: { 505 | type: [String, Object, Function] 506 | } 507 | }, 508 | created: function created() { 509 | if (typeof document === 'undefined') return; 510 | var el = document.querySelector(this.mountTo); 511 | 512 | if (!el) { 513 | console.error("[portal-vue]: Mount Point '".concat(this.mountTo, "' not found in document")); 514 | return; 515 | } 516 | 517 | var props = this.$props; // Target already exists 518 | 519 | if (wormhole.targets[props.name]) { 520 | if (props.bail) { 521 | console.warn("[portal-vue]: Target ".concat(props.name, " is already mounted.\n Aborting because 'bail: true' is set")); 522 | } else { 523 | this.portalTarget = wormhole.targets[props.name]; 524 | } 525 | 526 | return; 527 | } 528 | 529 | var append = props.append; 530 | 531 | if (append) { 532 | var type = typeof append === 'string' ? append : 'DIV'; 533 | var mountEl = document.createElement(type); 534 | el.appendChild(mountEl); 535 | el = mountEl; 536 | } // get props for target from $props 537 | // we have to rename a few of them 538 | 539 | 540 | var _props = pick(this.$props, targetProps); 541 | 542 | _props.slim = this.targetSlim; 543 | _props.tag = this.targetTag; 544 | _props.slotProps = this.targetSlotProps; 545 | _props.name = this.to; 546 | this.portalTarget = new PortalTarget({ 547 | el: el, 548 | parent: this.$parent || this, 549 | propsData: _props 550 | }); 551 | }, 552 | beforeDestroy: function beforeDestroy() { 553 | var target = this.portalTarget; 554 | 555 | if (this.append) { 556 | var el = target.$el; 557 | el.parentNode.removeChild(el); 558 | } 559 | 560 | target.$destroy(); 561 | }, 562 | render: function render(h) { 563 | if (!this.portalTarget) { 564 | console.warn("[portal-vue] Target wasn't mounted"); 565 | return h(); 566 | } // if there's no "manual" scoped slot, so we create a ourselves 567 | 568 | 569 | if (!this.$scopedSlots.manual) { 570 | var props = pick(this.$props, portalProps); 571 | return h(Portal, { 572 | props: props, 573 | attrs: this.$attrs, 574 | on: this.$listeners, 575 | scopedSlots: this.$scopedSlots 576 | }, this.$slots.default); 577 | } // else, we render the scoped slot 578 | 579 | 580 | var content = this.$scopedSlots.manual({ 581 | to: this.to 582 | }); // if user used