@bardoui/vpopper
Version:
PopperJs powered popup for vue 3
470 lines (453 loc) • 15.3 kB
JavaScript
import { reactive, defineComponent, ref, computed, toRaw, onMounted, provide, onUnmounted, h, withModifiers, inject, openBlock, createElementBlock, normalizeClass, renderSlot, createElementVNode } from 'vue';
import { popperGenerator, popperOffsets, preventOverflow, arrow, flip, computeStyles, applyStyles, eventListeners } from '@popperjs/core';
import anime from 'animejs';
/**
* Default popper options
*/
const GlobalOptions = reactive({
enterAnimation: {
top: {
translateY: [-20, 0],
opacity: [0, 1],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
left: {
translateX: [-20, 0],
opacity: [0, 1],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
bottom: {
translateY: [20, 0],
opacity: [0, 1],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
right: {
translateX: [20, 0],
opacity: [0, 1],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
},
leaveAnimation: {
top: {
translateY: [0, -20],
opacity: [1, 0],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
left: {
translateX: [0, -20],
opacity: [1, 0],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
bottom: {
translateY: [0, 20],
opacity: [1, 0],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
right: {
translateX: [0, 20],
opacity: [1, 0],
easing: "cubicBezier(0.165, 0.840, 0.440, 1.000)",
duration: 200,
},
},
});
/**
* set global popper options
* @param option new options
*/
function setGlobalOptions(option) {
const res = mergeOptions(GlobalOptions, option);
GlobalOptions.enterAnimation = res.enterAnimation;
GlobalOptions.leaveAnimation = res.leaveAnimation;
}
/**
* merge multiple popper options
* ignore undefined value
* latest option value is selected
*
* @param options options list to merge
* @returns merged options
*/
function mergeOptions(...options) {
const res = {};
for (const option of options) {
if (option.enterAnimation) {
res.enterAnimation = option.enterAnimation;
}
if (option.leaveAnimation) {
res.leaveAnimation = option.leaveAnimation;
}
}
return res;
}
// default create function
let instance = popperGenerator();
/**
* register popperjs creator instance
* @param instance createFunction instance
*/
function registerCreator(ins = popperGenerator()) {
instance = ins;
}
/**
* get default create function
*/
function createFunction() {
return instance;
}
/**
* Create default popperJs instance
* this instance includes this modifiers:
* popperOffsets
* preventOverflow
* arrow
* flip
* computeStyles
* applyStyles
* eventListeners
*/
function createDefaultPopper() {
registerCreator(popperGenerator({
defaultOptions: {
modifiers: [
{
name: "arrow",
options: {
padding: 7,
},
},
],
},
defaultModifiers: [
popperOffsets,
preventOverflow,
arrow,
flip,
computeStyles,
applyStyles,
eventListeners,
],
}));
}
/**
* check if animation object is of type PopupFullAnimation
* @param obj popup animation object
*/
function isPopperFullAnimation(obj) {
return (obj &&
obj.top &&
typeof obj.top === "object" &&
obj.left &&
typeof obj.left === "object" &&
obj.bottom &&
typeof obj.bottom === "object" &&
obj.right &&
typeof obj.right === "object");
}
/**
* get popperJs placement and return vPopper placement
* for auto and default state returns bottom
*
* @param placement popperJs placement
*/
function resolvePlacement(placement) {
if (placement.includes("top"))
return "top";
if (placement.includes("left"))
return "left";
if (placement.includes("right"))
return "right";
return "bottom";
}
/**
* get current animation based on placement
* undefined animation skipped
*
* @param placement popper placement
* @param animations animations list to search for animation state
*/
function resolveAnimation(placement, ...animations) {
let animation = {};
for (const anim of animations) {
if (anim) {
animation = anim;
}
}
if (isPopperFullAnimation(animation)) {
return animation[resolvePlacement(placement)];
}
return animation || {};
}
var script$2 = defineComponent({
name: "vPopper",
inheritAttrs: false,
emits: ["initialized", "show", "hide"],
props: {
tag: String,
trigger: String,
onAction: Function,
closable: {
type: Boolean,
default: true,
},
options: Object,
config: Object,
},
setup(props, { attrs, slots, emit }) {
// Stats and refs
const target = ref();
const popup = ref();
const animWrapper = ref();
const shown = ref(false);
const loading = ref(false);
const trigger = computed(() => props.trigger || "hover");
const option = computed(() => mergeOptions(toRaw(GlobalOptions), props.options || {}));
// Animations
let showAnim;
let hideAnim;
onMounted(() => {
if (!target.value || !popup.value || !animWrapper.value)
return;
const instance = createFunction()(target.value, popup.value, toRaw(props.config || {}));
emit("initialized", instance);
showAnim = anime(Object.assign(Object.assign({}, resolveAnimation(instance.state.options.placement, option.value.enterAnimation)), {
targets: animWrapper.value,
autoplay: false,
}));
hideAnim = anime(Object.assign(Object.assign({}, resolveAnimation(instance.state.options.placement, option.value.leaveAnimation)), {
targets: animWrapper.value,
autoplay: false,
}));
});
// Internals
let hiding = false;
let showCallback;
let hideCallback;
function doShow() {
if (!shown.value) {
shown.value = true;
hiding = false;
hideAnim === null || hideAnim === void 0 ? void 0 : hideAnim.pause();
if (showAnim) {
showAnim === null || showAnim === void 0 ? void 0 : showAnim.restart();
showAnim.finished.then(() => {
showCallback && showCallback();
emit("show");
});
}
else {
showCallback && showCallback();
emit("show");
}
}
}
function doHide(mode) {
if (shown.value && !hiding) {
hiding = true;
showAnim === null || showAnim === void 0 ? void 0 : showAnim.pause();
if (hideAnim) {
hideAnim.restart();
hideAnim.finished.then(() => {
shown.value = false;
hiding = false;
hideCallback && hideCallback(mode);
emit("hide", mode);
});
}
else {
shown.value = false;
hiding = false;
hideCallback && hideCallback(mode);
emit("hide", mode);
}
}
}
function doAction(key, data) {
if (props.onAction) {
loading.value = true;
props
.onAction(key, data)
.then((res) => {
loading.value = false;
if (res) {
doHide("action");
}
})
.catch(() => {
loading.value = false;
});
}
}
// Provide for child components
provide("v-popper-close", doHide);
provide("v-popper-action", doAction);
provide("v-popper-loading", loading);
provide("v-popper-on-show", (cb) => (showCallback = cb));
provide("v-popper-on-hide", (cb) => (hideCallback = cb));
// Handle Functionalities
const onTargetFocus = () => {
setTimeout(() => {
if (target.value && trigger.value === "focus") {
if (target.value.contains(document.activeElement)) {
doShow();
}
else {
doHide("blur");
}
}
});
};
const onOutsideClick = (e) => {
if (target.value && e.target) {
if (target.value.contains(e.target)) {
trigger.value === "click" && doShow();
}
else {
props.closable && doHide("blur");
}
}
};
const onTargetMouseLeave = () => trigger.value === "hover" && doHide("blur");
onMounted(() => {
if (target.value) {
document.addEventListener("focusin", onTargetFocus);
document.addEventListener("focusout", onTargetFocus);
document.addEventListener("click", onOutsideClick);
document.addEventListener("mousemove", onTargetMouseLeave);
}
});
onUnmounted(() => {
document.removeEventListener("focusin", onTargetFocus);
document.removeEventListener("focusout", onTargetFocus);
document.removeEventListener("click", onOutsideClick);
document.removeEventListener("mousemove", onTargetMouseLeave);
});
// Render
return () => [
h(props.tag || "span", Object.assign(Object.assign({ ref: target }, attrs), { onMousemove: withModifiers(() => trigger.value === "hover" && doShow(), ["stop"]) }), slots.default
? slots.default({
open: doShow,
close: doHide,
action: doAction,
loading: loading,
})
: h("span", "Put Content here!")),
h("div", {
ref: popup,
class: "v-popper-container",
style: { visibility: shown.value ? undefined : "hidden" },
onMousemove: withModifiers(() => ({}), ["stop"]),
onClick: withModifiers(() => ({}), ["stop"]),
}, h("div", {
ref: animWrapper,
}, slots.popup
? slots.popup()
: h("span", "put popup data in popup slot"))),
];
},
});
script$2.__file = "src/Popper.vue";
function usePopup() {
// Stats
const fallbackCB = () => console.error("Use popup plugin inside popper component!");
// injects
const close = inject("v-popper-close", fallbackCB);
const action = inject("v-popper-action", fallbackCB);
const loading = inject("v-popper-loading", ref(false));
const onShow = inject("v-popper-on-show", fallbackCB);
const onHide = inject("v-popper-on-hide", fallbackCB);
return { close, action, loading, onShow, onHide };
}
var script$1 = defineComponent({
name: "vPopup",
emits: ["show", "hide"],
props: {
closable: { type: Boolean, default: true },
},
setup(props, { emit }) {
const { close, action, loading, onShow, onHide } = usePopup();
onShow(() => emit("show"));
onHide((mode) => emit("hide", mode));
function dismiss() {
props.closable && close("click");
}
return { loading, action, close, dismiss };
},
});
const _hoisted_1$1 = /*#__PURE__*/createElementVNode("div", {
class: "arrow",
"data-popper-arrow": ""
}, null, -1 /* HOISTED */);
function render$1(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createElementBlock("div", {
class: normalizeClass(["v-popup", { 'is-loading': _ctx.loading }]),
onClick: _cache[0] || (_cache[0] = withModifiers((...args) => (_ctx.dismiss && _ctx.dismiss(...args)), ["stop"]))
}, [
renderSlot(_ctx.$slots, "default", {
action: _ctx.action,
close: _ctx.close,
loading: _ctx.loading
}),
_hoisted_1$1
], 2 /* CLASS */))
}
script$1.render = render$1;
script$1.__file = "src/Popup.vue";
var script = defineComponent({
name: "vPopup",
emits: ["show", "hide"],
props: {
closable: { type: Boolean, default: true },
},
setup(props, { emit }) {
const { close, action, loading, onShow, onHide } = usePopup();
onShow(() => emit("show"));
onHide((mode) => emit("hide", mode));
function dismiss() {
props.closable && close("click");
}
return { loading, action, close, dismiss };
},
});
const _hoisted_1 = /*#__PURE__*/createElementVNode("div", {
class: "arrow",
"data-popper-arrow": ""
}, null, -1 /* HOISTED */);
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createElementBlock("div", {
class: normalizeClass(["v-tooltip", { 'is-loading': _ctx.loading }]),
onClick: _cache[0] || (_cache[0] = withModifiers((...args) => (_ctx.dismiss && _ctx.dismiss(...args)), ["stop"]))
}, [
renderSlot(_ctx.$slots, "default", {
action: _ctx.action,
close: _ctx.close,
loading: _ctx.loading
}),
_hoisted_1
], 2 /* CLASS */))
}
script.render = render;
script.__file = "src/Tooltip.vue";
/**
* Create default popperJs instance by default
*/
createDefaultPopper();
/**
* install popper plugin
*/
var vPopper = {
install: (app) => {
app.component("popper", script$2);
app.component("popup", script$1);
app.component("tooltip", script);
},
};
export { script$2 as Popper, script$1 as Popup, script as Tooltip, createDefaultPopper, createFunction, vPopper as default, registerCreator, setGlobalOptions, usePopup };
//# sourceMappingURL=vpopper.esm.js.map