Description
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.

179 lines
5.5 KiB

4 years ago
import Vue from '../vue';
import { NAME_TRANSPORTER_SINGLE, NAME_TRANSPORTER_TARGET_SINGLE } from '../constants/components';
import identity from './identity';
import { concat } from './array';
import { removeNode, select } from './dom';
import { isBrowser } from './env';
import { isFunction, isString } from './inspect';
import { HTMLElement } from './safe-types';
import normalizeSlotMixin from '../mixins/normalize-slot'; // BTransporterSingle/BTransporterTargetSingle:
//
// Single root node portaling of content, which retains parent/child hierarchy
// Unlike Portal-Vue where portaled content is no longer a descendent of its
// intended parent components
//
// Private components for use by Tooltips, Popovers and Modals
//
// Based on vue-simple-portal
// https://github.com/LinusBorg/vue-simple-portal
// Transporter target used by BTransporterSingle
// Supports only a single root element
// @vue/component
var BTransporterTargetSingle = /*#__PURE__*/Vue.extend({
// As an abstract component, it doesn't appear in the $parent chain of
// components, which means the next parent of any component rendered inside
// of this one will be the parent from which is was portal'd
abstract: true,
name: NAME_TRANSPORTER_TARGET_SINGLE,
props: {
nodes: {
// Even though we only support a single root element,
// VNodes are always passed as an array
type: [Array, Function] // default: undefined
}
},
data: function data(vm) {
return {
updatedNodes: vm.nodes
};
},
destroyed: function destroyed() {
removeNode(this.$el);
},
render: function render(h) {
var nodes = isFunction(this.updatedNodes) ? this.updatedNodes({}) : this.updatedNodes;
nodes = concat(nodes).filter(Boolean);
/* istanbul ignore else */
if (nodes && nodes.length > 0 && !nodes[0].text) {
return nodes[0];
} else {
/* istanbul ignore next */
return h();
}
}
}); // This component has no root element, so only a single VNode is allowed
// @vue/component
export var BTransporterSingle = /*#__PURE__*/Vue.extend({
name: NAME_TRANSPORTER_SINGLE,
mixins: [normalizeSlotMixin],
props: {
disabled: {
type: Boolean,
default: false
},
container: {
// String: CSS selector,
// HTMLElement: Element reference
// Mainly needed for tooltips/popovers inside modals
type: [String, HTMLElement],
default: 'body'
},
tag: {
// This should be set to match the root element type
type: String,
default: 'div'
}
},
watch: {
disabled: {
immediate: true,
handler: function handler(disabled) {
disabled ? this.unmountTarget() : this.$nextTick(this.mountTarget);
}
}
},
created: function created() {
// Create private non-reactive props
this.$_defaultFn = null;
this.$_target = null;
},
beforeMount: function beforeMount() {
this.mountTarget();
},
updated: function updated() {
// We need to make sure that all children have completed updating
// before rendering in the target
// `vue-simple-portal` has the this in a `$nextTick()`,
// while `portal-vue` doesn't
// Just trying to see if the `$nextTick()` delay is required or not
// Since all slots in Vue 2.6.x are always functions
this.updateTarget();
},
beforeDestroy: function beforeDestroy() {
this.unmountTarget();
this.$_defaultFn = null;
},
methods: {
// Get the element which the target should be appended to
getContainer: function getContainer() {
/* istanbul ignore else */
if (isBrowser) {
var container = this.container;
return isString(container) ? select(container) : container;
} else {
return null;
}
},
// Mount the target
mountTarget: function mountTarget() {
if (!this.$_target) {
var container = this.getContainer();
if (container) {
var el = document.createElement('div');
container.appendChild(el);
this.$_target = new BTransporterTargetSingle({
el: el,
parent: this,
propsData: {
// Initial nodes to be rendered
nodes: concat(this.normalizeSlot())
}
});
}
}
},
// Update the content of the target
updateTarget: function updateTarget() {
if (isBrowser && this.$_target) {
var defaultFn = this.$scopedSlots.default;
if (!this.disabled) {
/* istanbul ignore else: only applicable in Vue 2.5.x */
if (defaultFn && this.$_defaultFn !== defaultFn) {
// We only update the target component if the scoped slot
// function is a fresh one. The new slot syntax (since Vue 2.6)
// can cache unchanged slot functions and we want to respect that here
this.$_target.updatedNodes = defaultFn;
} else if (!defaultFn) {
// We also need to be back compatible with non-scoped default slot (i.e. 2.5.x)
this.$_target.updatedNodes = this.$slots.default;
}
} // Update the scoped slot function cache
this.$_defaultFn = defaultFn;
}
},
// Unmount the target
unmountTarget: function unmountTarget() {
this.$_target && this.$_target.$destroy();
this.$_target = null;
}
},
render: function render(h) {
if (this.disabled) {
var nodes = concat(this.normalizeSlot()).filter(identity);
if (nodes.length > 0 && !nodes[0].text) {
return nodes[0];
}
}
return h();
}
});