You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
626 lines
17 KiB
626 lines
17 KiB
|
|
/*!
|
|
* portal-vue © Thorsten Lünborg, 2019
|
|
*
|
|
* Version: 2.1.7
|
|
*
|
|
* LICENCE: MIT
|
|
*
|
|
* https://github.com/linusborg/portal-vue
|
|
*
|
|
*/
|
|
|
|
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'vue'], factory) :
|
|
(factory((global.PortalVue = {}),global.Vue));
|
|
}(this, (function (exports,Vue) { 'use strict';
|
|
|
|
Vue = Vue && Vue.hasOwnProperty('default') ? Vue['default'] : Vue;
|
|
|
|
function _typeof(obj) {
|
|
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
|
|
_typeof = function (obj) {
|
|
return typeof obj;
|
|
};
|
|
} else {
|
|
_typeof = function (obj) {
|
|
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
|
};
|
|
}
|
|
|
|
return _typeof(obj);
|
|
}
|
|
|
|
function _toConsumableArray(arr) {
|
|
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
|
|
}
|
|
|
|
function _arrayWithoutHoles(arr) {
|
|
if (Array.isArray(arr)) {
|
|
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
|
|
|
|
return arr2;
|
|
}
|
|
}
|
|
|
|
function _iterableToArray(iter) {
|
|
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
|
|
}
|
|
|
|
function _nonIterableSpread() {
|
|
throw new TypeError("Invalid attempt to spread non-iterable instance");
|
|
}
|
|
|
|
var inBrowser = typeof window !== 'undefined';
|
|
function freeze(item) {
|
|
if (Array.isArray(item) || _typeof(item) === 'object') {
|
|
return Object.freeze(item);
|
|
}
|
|
|
|
return item;
|
|
}
|
|
function combinePassengers(transports) {
|
|
var slotProps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
return transports.reduce(function (passengers, transport) {
|
|
var temp = transport.passengers[0];
|
|
var newPassengers = typeof temp === 'function' ? temp(slotProps) : transport.passengers;
|
|
return passengers.concat(newPassengers);
|
|
}, []);
|
|
}
|
|
function stableSort(array, compareFn) {
|
|
return array.map(function (v, idx) {
|
|
return [idx, v];
|
|
}).sort(function (a, b) {
|
|
return compareFn(a[1], b[1]) || a[0] - b[0];
|
|
}).map(function (c) {
|
|
return c[1];
|
|
});
|
|
}
|
|
function pick(obj, keys) {
|
|
return keys.reduce(function (acc, key) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
acc[key] = obj[key];
|
|
}
|
|
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
var transports = {};
|
|
var targets = {};
|
|
var sources = {};
|
|
var Wormhole = Vue.extend({
|
|
data: function data() {
|
|
return {
|
|
transports: transports,
|
|
targets: targets,
|
|
sources: sources,
|
|
trackInstances: inBrowser
|
|
};
|
|
},
|
|
methods: {
|
|
open: function open(transport) {
|
|
if (!inBrowser) return;
|
|
var to = transport.to,
|
|
from = transport.from,
|
|
passengers = transport.passengers,
|
|
_transport$order = transport.order,
|
|
order = _transport$order === void 0 ? Infinity : _transport$order;
|
|
if (!to || !from || !passengers) return;
|
|
var newTransport = {
|
|
to: to,
|
|
from: from,
|
|
passengers: freeze(passengers),
|
|
order: order
|
|
};
|
|
var keys = Object.keys(this.transports);
|
|
|
|
if (keys.indexOf(to) === -1) {
|
|
Vue.set(this.transports, to, []);
|
|
}
|
|
|
|
var currentIndex = this.$_getTransportIndex(newTransport); // Copying the array here so that the PortalTarget change event will actually contain two distinct arrays
|
|
|
|
var newTransports = this.transports[to].slice(0);
|
|
|
|
if (currentIndex === -1) {
|
|
newTransports.push(newTransport);
|
|
} else {
|
|
newTransports[currentIndex] = newTransport;
|
|
}
|
|
|
|
this.transports[to] = stableSort(newTransports, function (a, b) {
|
|
return a.order - b.order;
|
|
});
|
|
},
|
|
close: function close(transport) {
|
|
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
var to = transport.to,
|
|
from = transport.from;
|
|
if (!to || !from && force === false) return;
|
|
|
|
if (!this.transports[to]) {
|
|
return;
|
|
}
|
|
|
|
if (force) {
|
|
this.transports[to] = [];
|
|
} else {
|
|
var index = this.$_getTransportIndex(transport);
|
|
|
|
if (index >= 0) {
|
|
// Copying the array here so that the PortalTarget change event will actually contain two distinct arrays
|
|
var newTransports = this.transports[to].slice(0);
|
|
newTransports.splice(index, 1);
|
|
this.transports[to] = newTransports;
|
|
}
|
|
}
|
|
},
|
|
registerTarget: function registerTarget(target, vm, force) {
|
|
if (!inBrowser) return;
|
|
|
|
if (this.trackInstances && !force && this.targets[target]) {
|
|
console.warn("[portal-vue]: Target ".concat(target, " already exists"));
|
|
}
|
|
|
|
this.$set(this.targets, target, Object.freeze([vm]));
|
|
},
|
|
unregisterTarget: function unregisterTarget(target) {
|
|
this.$delete(this.targets, target);
|
|
},
|
|
registerSource: function registerSource(source, vm, force) {
|
|
if (!inBrowser) return;
|
|
|
|
if (this.trackInstances && !force && this.sources[source]) {
|
|
console.warn("[portal-vue]: source ".concat(source, " already exists"));
|
|
}
|
|
|
|
this.$set(this.sources, source, Object.freeze([vm]));
|
|
},
|
|
unregisterSource: function unregisterSource(source) {
|
|
this.$delete(this.sources, source);
|
|
},
|
|
hasTarget: function hasTarget(to) {
|
|
return !!(this.targets[to] && this.targets[to][0]);
|
|
},
|
|
hasSource: function hasSource(to) {
|
|
return !!(this.sources[to] && this.sources[to][0]);
|
|
},
|
|
hasContentFor: function hasContentFor(to) {
|
|
return !!this.transports[to] && !!this.transports[to].length;
|
|
},
|
|
// Internal
|
|
$_getTransportIndex: function $_getTransportIndex(_ref) {
|
|
var to = _ref.to,
|
|
from = _ref.from;
|
|
|
|
for (var i in this.transports[to]) {
|
|
if (this.transports[to][i].from === from) {
|
|
return +i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
});
|
|
var wormhole = new Wormhole(transports);
|
|
|
|
var _id = 1;
|
|
var Portal = Vue.extend({
|
|
name: 'portal',
|
|
props: {
|
|
disabled: {
|
|
type: Boolean
|
|
},
|
|
name: {
|
|
type: String,
|
|
default: function _default() {
|
|
return String(_id++);
|
|
}
|
|
},
|
|
order: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
slim: {
|
|
type: Boolean
|
|
},
|
|
slotProps: {
|
|
type: Object,
|
|
default: function _default() {
|
|
return {};
|
|
}
|
|
},
|
|
tag: {
|
|
type: String,
|
|
default: 'DIV'
|
|
},
|
|
to: {
|
|
type: String,
|
|
default: function _default() {
|
|
return String(Math.round(Math.random() * 10000000));
|
|
}
|
|
}
|
|
},
|
|
created: function created() {
|
|
var _this = this;
|
|
|
|
this.$nextTick(function () {
|
|
wormhole.registerSource(_this.name, _this);
|
|
});
|
|
},
|
|
mounted: function mounted() {
|
|
if (!this.disabled) {
|
|
this.sendUpdate();
|
|
}
|
|
},
|
|
updated: function updated() {
|
|
if (this.disabled) {
|
|
this.clear();
|
|
} else {
|
|
this.sendUpdate();
|
|
}
|
|
},
|
|
beforeDestroy: function beforeDestroy() {
|
|
wormhole.unregisterSource(this.name);
|
|
this.clear();
|
|
},
|
|
watch: {
|
|
to: function to(newValue, oldValue) {
|
|
oldValue && oldValue !== newValue && this.clear(oldValue);
|
|
this.sendUpdate();
|
|
}
|
|
},
|
|
methods: {
|
|
clear: function clear(target) {
|
|
var closer = {
|
|
from: this.name,
|
|
to: target || this.to
|
|
};
|
|
wormhole.close(closer);
|
|
},
|
|
normalizeSlots: function normalizeSlots() {
|
|
return this.$scopedSlots.default ? [this.$scopedSlots.default] : this.$slots.default;
|
|
},
|
|
normalizeOwnChildren: function normalizeOwnChildren(children) {
|
|
return typeof children === 'function' ? children(this.slotProps) : children;
|
|
},
|
|
sendUpdate: function sendUpdate() {
|
|
var slotContent = this.normalizeSlots();
|
|
|
|
if (slotContent) {
|
|
var transport = {
|
|
from: this.name,
|
|
to: this.to,
|
|
passengers: _toConsumableArray(slotContent),
|
|
order: this.order
|
|
};
|
|
wormhole.open(transport);
|
|
} else {
|
|
this.clear();
|
|
}
|
|
}
|
|
},
|
|
render: function render(h) {
|
|
var children = this.$slots.default || this.$scopedSlots.default || [];
|
|
var Tag = this.tag;
|
|
|
|
if (children && this.disabled) {
|
|
return children.length <= 1 && this.slim ? this.normalizeOwnChildren(children)[0] : h(Tag, [this.normalizeOwnChildren(children)]);
|
|
} else {
|
|
return this.slim ? h() : h(Tag, {
|
|
class: {
|
|
'v-portal': true
|
|
},
|
|
style: {
|
|
display: 'none'
|
|
},
|
|
key: 'v-portal-placeholder'
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
var PortalTarget = Vue.extend({
|
|
name: 'portalTarget',
|
|
props: {
|
|
multiple: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
name: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
slim: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
slotProps: {
|
|
type: Object,
|
|
default: function _default() {
|
|
return {};
|
|
}
|
|
},
|
|
tag: {
|
|
type: String,
|
|
default: 'div'
|
|
},
|
|
transition: {
|
|
type: [String, Object, Function]
|
|
}
|
|
},
|
|
data: function data() {
|
|
return {
|
|
transports: wormhole.transports,
|
|
firstRender: true
|
|
};
|
|
},
|
|
created: function created() {
|
|
var _this = this;
|
|
|
|
this.$nextTick(function () {
|
|
wormhole.registerTarget(_this.name, _this);
|
|
});
|
|
},
|
|
watch: {
|
|
ownTransports: function ownTransports() {
|
|
this.$emit('change', this.children().length > 0);
|
|
},
|
|
name: function name(newVal, oldVal) {
|
|
/**
|
|
* TODO
|
|
* This should warn as well ...
|
|
*/
|
|
wormhole.unregisterTarget(oldVal);
|
|
wormhole.registerTarget(newVal, this);
|
|
}
|
|
},
|
|
mounted: function mounted() {
|
|
var _this2 = this;
|
|
|
|
if (this.transition) {
|
|
this.$nextTick(function () {
|
|
// only when we have a transition, because it causes a re-render
|
|
_this2.firstRender = false;
|
|
});
|
|
}
|
|
},
|
|
beforeDestroy: function beforeDestroy() {
|
|
wormhole.unregisterTarget(this.name);
|
|
},
|
|
computed: {
|
|
ownTransports: function ownTransports() {
|
|
var transports = this.transports[this.name] || [];
|
|
|
|
if (this.multiple) {
|
|
return transports;
|
|
}
|
|
|
|
return transports.length === 0 ? [] : [transports[transports.length - 1]];
|
|
},
|
|
passengers: function passengers() {
|
|
return combinePassengers(this.ownTransports, this.slotProps);
|
|
}
|
|
},
|
|
methods: {
|
|
// can't be a computed prop because it has to "react" to $slot changes.
|
|
children: function children() {
|
|
return this.passengers.length !== 0 ? this.passengers : this.$scopedSlots.default ? this.$scopedSlots.default(this.slotProps) : this.$slots.default || [];
|
|
},
|
|
// can't be a computed prop because it has to "react" to this.children().
|
|
noWrapper: function noWrapper() {
|
|
var noWrapper = this.slim && !this.transition;
|
|
|
|
if (noWrapper && this.children().length > 1) {
|
|
console.warn('[portal-vue]: PortalTarget with `slim` option received more than one child element.');
|
|
}
|
|
|
|
return noWrapper;
|
|
}
|
|
},
|
|
render: function render(h) {
|
|
var noWrapper = this.noWrapper();
|
|
var children = this.children();
|
|
var Tag = this.transition || this.tag;
|
|
return noWrapper ? children[0] : this.slim && !Tag ? h() : h(Tag, {
|
|
props: {
|
|
// if we have a transition component, pass the tag if it exists
|
|
tag: this.transition && this.tag ? this.tag : undefined
|
|
},
|
|
class: {
|
|
'vue-portal-target': true
|
|
}
|
|
}, children);
|
|
}
|
|
});
|
|
|
|
var _id$1 = 0;
|
|
var portalProps = ['disabled', 'name', 'order', 'slim', 'slotProps', 'tag', 'to'];
|
|
var targetProps = ['multiple', 'transition'];
|
|
var MountingPortal = Vue.extend({
|
|
name: 'MountingPortal',
|
|
inheritAttrs: false,
|
|
props: {
|
|
append: {
|
|
type: [Boolean, String]
|
|
},
|
|
bail: {
|
|
type: Boolean
|
|
},
|
|
mountTo: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
// Portal
|
|
disabled: {
|
|
type: Boolean
|
|
},
|
|
// name for the portal
|
|
name: {
|
|
type: String,
|
|
default: function _default() {
|
|
return 'mounted_' + String(_id$1++);
|
|
}
|
|
},
|
|
order: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
slim: {
|
|
type: Boolean
|
|
},
|
|
slotProps: {
|
|
type: Object,
|
|
default: function _default() {
|
|
return {};
|
|
}
|
|
},
|
|
tag: {
|
|
type: String,
|
|
default: 'DIV'
|
|
},
|
|
// name for the target
|
|
to: {
|
|
type: String,
|
|
default: function _default() {
|
|
return String(Math.round(Math.random() * 10000000));
|
|
}
|
|
},
|
|
// Target
|
|
multiple: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
targetSlim: {
|
|
type: Boolean
|
|
},
|
|
targetSlotProps: {
|
|
type: Object,
|
|
default: function _default() {
|
|
return {};
|
|
}
|
|
},
|
|
targetTag: {
|
|
type: String,
|
|
default: 'div'
|
|
},
|
|
transition: {
|
|
type: [String, Object, Function]
|
|
}
|
|
},
|
|
created: function created() {
|
|
if (typeof document === 'undefined') return;
|
|
var el = document.querySelector(this.mountTo);
|
|
|
|
if (!el) {
|
|
console.error("[portal-vue]: Mount Point '".concat(this.mountTo, "' not found in document"));
|
|
return;
|
|
}
|
|
|
|
var props = this.$props; // Target already exists
|
|
|
|
if (wormhole.targets[props.name]) {
|
|
if (props.bail) {
|
|
console.warn("[portal-vue]: Target ".concat(props.name, " is already mounted.\n Aborting because 'bail: true' is set"));
|
|
} else {
|
|
this.portalTarget = wormhole.targets[props.name];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var append = props.append;
|
|
|
|
if (append) {
|
|
var type = typeof append === 'string' ? append : 'DIV';
|
|
var mountEl = document.createElement(type);
|
|
el.appendChild(mountEl);
|
|
el = mountEl;
|
|
} // get props for target from $props
|
|
// we have to rename a few of them
|
|
|
|
|
|
var _props = pick(this.$props, targetProps);
|
|
|
|
_props.slim = this.targetSlim;
|
|
_props.tag = this.targetTag;
|
|
_props.slotProps = this.targetSlotProps;
|
|
_props.name = this.to;
|
|
this.portalTarget = new PortalTarget({
|
|
el: el,
|
|
parent: this.$parent || this,
|
|
propsData: _props
|
|
});
|
|
},
|
|
beforeDestroy: function beforeDestroy() {
|
|
var target = this.portalTarget;
|
|
|
|
if (this.append) {
|
|
var el = target.$el;
|
|
el.parentNode.removeChild(el);
|
|
}
|
|
|
|
target.$destroy();
|
|
},
|
|
render: function render(h) {
|
|
if (!this.portalTarget) {
|
|
console.warn("[portal-vue] Target wasn't mounted");
|
|
return h();
|
|
} // if there's no "manual" scoped slot, so we create a <Portal> ourselves
|
|
|
|
|
|
if (!this.$scopedSlots.manual) {
|
|
var props = pick(this.$props, portalProps);
|
|
return h(Portal, {
|
|
props: props,
|
|
attrs: this.$attrs,
|
|
on: this.$listeners,
|
|
scopedSlots: this.$scopedSlots
|
|
}, this.$slots.default);
|
|
} // else, we render the scoped slot
|
|
|
|
|
|
var content = this.$scopedSlots.manual({
|
|
to: this.to
|
|
}); // if user used <template> for the scoped slot
|
|
// content will be an array
|
|
|
|
if (Array.isArray(content)) {
|
|
content = content[0];
|
|
}
|
|
|
|
if (!content) return h();
|
|
return content;
|
|
}
|
|
});
|
|
|
|
function install(Vue$$1) {
|
|
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
Vue$$1.component(options.portalName || 'Portal', Portal);
|
|
Vue$$1.component(options.portalTargetName || 'PortalTarget', PortalTarget);
|
|
Vue$$1.component(options.MountingPortalName || 'MountingPortal', MountingPortal);
|
|
}
|
|
|
|
if ( // @ts-ignore
|
|
typeof window !== 'undefined' && window.Vue && window.Vue === Vue) {
|
|
window.Vue.use({
|
|
install: install
|
|
});
|
|
}
|
|
|
|
var index = {
|
|
install: install
|
|
};
|
|
|
|
exports.default = index;
|
|
exports.Portal = Portal;
|
|
exports.PortalTarget = PortalTarget;
|
|
exports.MountingPortal = MountingPortal;
|
|
exports.Wormhole = wormhole;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
})));
|
|
|