gw-popup
Version:
A configurable popup component for Vue 3
806 lines (805 loc) • 25.9 kB
JavaScript
import { useEventListener as w } from "@vueuse/core";
import { defineComponent as S, inject as C, ref as P, openBlock as u, createBlock as $, Teleport as D, createElementBlock as k, normalizeClass as H, createElementVNode as y, normalizeStyle as x, Fragment as q, renderList as B, createCommentVNode as R, renderSlot as j, resolveComponent as A, withCtx as N, toDisplayString as z } from "vue";
import { defineStore as I, createPinia as U } from "pinia";
const _ = S({
name: "PopUp",
setup(t) {
const i = C("popupManager");
if (!i)
throw new Error("PopupManager not provided");
const s = {
top: 0,
left: 0,
width: 0,
height: 0
}, a = {}, l = {}, h = {
backgroundFill: "transparent",
popupPosition: s,
showRequested: !1,
hideRequested: !1,
isDisplayed: !1,
isInit: !1,
popupId: "",
retries: 0,
positioner: null,
svgParams: null
}, n = {
current: {
left: 0,
top: 0
},
original: {
left: 0,
top: 0
}
}, o = {
arrowDistance: 2,
arrowHeight: 7,
arrowOffset: 12,
arrowPosition: "center",
arrowWidth: 9,
class: "",
cornerRadius: 3,
debugMode: !1,
direction: "e",
eventOff: "mouseleave",
eventOn: "mouseenter",
isManual: !1,
isModal: !1,
isPositioned: !0,
isTooltip: !0,
logging: !1,
paddingX: 6,
paddingY: 2,
popupOffset: 0,
popupPosition: "center",
positioner: "",
shiftX: 0,
shiftY: 0,
zIndex: 100
};
h.popupId = t.id;
const g = i.mergeConfig(
t.mode,
o,
t.props
);
return {
data: P(h),
shifts: P(n),
styles: P({
content: a,
element: l
}),
config: g,
closeHandle: () => {
},
popupManager: i
};
},
props: {
id: {
type: String,
default: null
},
mode: {
type: String,
required: !0
},
props: {
type: Object,
default: () => {
}
},
refSource: {
type: Object,
default: null
},
autoOpen: {
type: Boolean,
default: !1
}
},
methods: {
getFill(t) {
var e, i;
return ((e = this.data.svgParams) == null ? void 0 : e.paths[t].fill) === "backgroundColor" ? this.data.backgroundFill : (i = this.data.svgParams) == null ? void 0 : i.paths[t].fill;
},
forceHide() {
this.data.isDisplayed && !this.config.isModal && (this.styles.element = { top: "-3000px", left: "-3000px" }), this.data.hideRequested = !1, this.data.showRequested = !1, this.data.isDisplayed = !1, this.popupManager.setStatus(this.data.popupId, "isOpen", !1), this.closeHandle();
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
refresh(t) {
this.data.isDisplayed && this.data.positioner && this.show({ positioner: this.data.positioner });
},
log(t) {
this.config.logging === !0 && console.log(t);
},
show(t) {
var i, s, a;
this.log("show: " + this.data.popupId);
let e = null;
if (this.config.isPositioned) {
const l = t.positioner;
typeof l == "string" ? (e = (i = this.$parent) == null ? void 0 : i.$refs[l], typeof e < "u" ? this.data.positioner = e : e = (a = (s = this.$parent) == null ? void 0 : s.$parent) == null ? void 0 : a.$refs[l]) : e = l, e || this.log("Popup.vue could not find positioner for id: " + this.id);
let h = e;
this.data.showRequested && (this.data.hideRequested = !1), this.data.showRequested = !0;
const n = setTimeout(() => {
this.data.showRequested = !1, this.data.hideRequested && (this.hide({}), this.log("hide requested")), clearTimeout(n);
}, 200);
this.data.isDisplayed = !0, this.popupManager.closeOpenTooltips(this.data.popupId);
let o;
const g = this.$refs.popupContent;
let m = g.children;
if (typeof m > "u")
return;
m = g.children;
try {
o = {
positioner: h.getBoundingClientRect(),
content: m[0].getBoundingClientRect(),
window: this.popupManager.percentHeightWidth(100)
};
} catch (f) {
this.log(
`the content or positioner probably no longer rendered, so close:
` + f
), this.hide({});
return;
}
if (o.positioner.top > 2e4) {
const f = setTimeout(() => {
this.data.retries++, this.data.retries > 1 ? this.log("we are hosed on popup retries") : this.show({ positioner: this.data.positioner }), clearTimeout(f);
}, 100);
}
let c = { left: 0, top: 0 };
this.data.retries = 0;
const v = g.children[0];
this.data.backgroundFill = window.getComputedStyle(v).backgroundColor;
let p = this.config.direction;
t.direction !== null && typeof t.direction < "u" && (p = t.direction), this.data.isInit ? c = {
left: this.shifts.current.left,
top: this.shifts.current.top
} : (this.shifts.original.left = 0, this.shifts.original.top = 0, this.shifts.current.left = this.config.shiftX, this.shifts.current.top = this.config.shiftY, this.data.isInit = !0);
let b = !1, r = {}, d = { top: 0, left: 0 }, O = 0;
do {
switch (b = !1, this.log("Direction: " + this.config.direction), p) {
case "n":
r = {
left: this.getPos(o, p),
top: o.positioner.top - this.config.arrowDistance - (o.content.height + 2 * this.config.paddingY + this.config.arrowHeight),
width: o.content.width + 2 * this.config.paddingX,
height: o.content.height + 2 * this.config.paddingY + this.config.arrowHeight
}, d.left = this.config.paddingX, d.top = this.config.paddingY;
break;
case "e":
r = {
left: o.positioner.right + this.config.arrowDistance,
top: this.getPos(o, p),
width: o.content.width + this.config.arrowWidth + 2 * this.config.paddingX,
height: o.content.height + 2 * this.config.paddingY
}, d.left = this.config.arrowHeight + this.config.paddingX, d.top = this.config.paddingY;
break;
case "s":
r = {
left: this.getPos(o, p),
top: o.positioner.top + o.positioner.height,
width: o.content.width + 2 * this.config.paddingX,
height: o.content.height + 2 * this.config.paddingY + this.config.arrowHeight
}, d.left = this.config.paddingX, d.top = this.config.paddingY + this.config.arrowHeight;
break;
case "w":
r = {
left: o.positioner.left - o.content.width - 2 * this.config.paddingX - this.config.arrowDistance - this.config.arrowHeight,
top: this.getPos(o, p),
width: o.content.width + 2 * this.config.paddingX + this.config.arrowHeight,
height: o.content.height + 2 * this.config.paddingY
}, d.left = this.config.paddingX, d.top = this.config.paddingY;
break;
}
r.top + r.height > o.window.height && (c.top = c.top - (o.window.height - (r.top + r.height)) + this.popupManager.percentHeightWidth(10).height, b = !0);
const f = p === "e" || p === "w";
r.top + r.height < o.positioner.top - (f ? o.positioner.height / 2 : this.popupManager.percentHeightWidth(10).height) && (c = {
top: this.shifts.original.top,
left: this.shifts.original.left
}, b = !0), b && (this.shifts.current.left = c.left, this.shifts.current.top = c.top), O++;
} while (b && O < 2);
let W = {
width: r.width + "px",
height: r.height + "px",
top: r.top + "px",
left: r.left + "px",
zIndex: this.config.zIndex
};
this.data.popupPosition = r;
let T = 0, M = "height";
switch (p) {
case "n":
case "s":
M = "width";
break;
}
switch (this.config.arrowPosition) {
case "min":
T = this.config.arrowDistance;
break;
case "center":
T = r[M] / 2;
break;
case "max":
T = r[M] - this.config.arrowDistance;
break;
}
this.data.svgParams = this.popupManager.buildArrowBox({
mode: this.mode,
arrowHeight: this.config.arrowHeight,
arrowWidth: this.config.arrowWidth,
arrowPosition: T,
height: r.height,
width: r.width,
cornerRadius: this.config.cornerRadius,
direction: p,
refX: o.positioner.x,
refY: o.positioner.y,
refWidth: o.positioner.width,
refHeight: o.positioner.height
}), this.styles.element = W, this.styles.content = {
left: d.left + "px",
top: d.top + "px"
}, this.config.isTooltip && (this.closeHandle = w(
window,
"mouseMove",
(f) => {
(Math.abs(f.clientX - r.left) > 200 || Math.abs(f.clientY - r.top) > 200) && (this.forceHide(), this.log("force hidden"));
}
));
} else
this.styles.element = {}, this.data.isDisplayed = !0, this.$nextTick(() => {
const l = this.$refs.popupContent;
let h = l.children;
if (typeof h > "u")
return;
h = l.children;
const n = h[0].getBoundingClientRect();
this.styles.content = {
width: n.width + 2 * this.config.paddingX + "px",
height: n.height + 2 * this.config.paddingY + "px",
background: window.getComputedStyle(h[0]).backgroundColor,
padding: this.config.paddingY + "px " + (this.config.paddingX + "px")
};
});
},
getPos(t, e) {
const i = ["n", "s"].indexOf(e) > -1;
let s = i ? "width" : "height", a = this.config[i ? "paddingX" : "paddingY"];
const l = t.positioner, h = t.content;
let n = l[i ? "x" : "y"];
switch (this.config.popupPosition) {
case "max":
n = n + l[s] + this.config.popupOffset - h[s] - a;
break;
case "center":
n = n + (l[s] - h[s] - a * 2) / 2;
break;
case "min":
n = n - a - this.config.popupOffset;
break;
}
return n;
},
hide(t) {
if (this.log("hide: " + this.data.popupId), !this.config.isPositioned) {
this.data.isDisplayed = !1;
return;
}
if (!this.data.isDisplayed)
return;
if (this.data.showRequested) {
this.data.hideRequested = !0;
return;
}
const e = setTimeout(() => {
if (this.data.showRequested)
clearTimeout(e), this.data.hideRequested = !1;
else {
let i = this.data.popupPosition;
i.top = this.config.debugMode ? 300 : -3e3, i.left = this.config.debugMode ? 300 : -3e3;
let s = {
width: i.width + "px",
height: i.height + "px",
top: i.top + "px",
left: i.left + "px",
zIndex: this.config.zIndex
};
this.styles.element = s, this.data.isDisplayed = !1, this.popupManager.setStatus(this.data.popupId, "isOpen", !1), this.closeHandle();
}
}, 2e3);
this.data.hideRequested = !0;
},
startUp(t) {
var l, h;
const e = {
show: this.show,
hide: this.hide,
refresh: this.refresh
};
let i = null, s = null;
if (!this.config.isManual) {
let n = null;
if (typeof ((l = this.$parent) == null ? void 0 : l.$refs[this.config.positioner]) < "u" && (s = (h = this.$parent) == null ? void 0 : h.$refs[this.config.positioner], s && (i = s), i !== null && (n = i.getBoundingClientRect())), n === null) {
t++, t < 11 ? setTimeout(() => {
this.startUp(t);
}, 20) : this.log("PopUp.vue could not find ref: " + this.config.positioner);
return;
} else
this.log(
"Popup found positioner: " + this.config.positioner + " on try " + t
);
}
const a = {
isTooltip: this.config.isTooltip,
isModal: this.config.isModal,
isManual: this.config.isManual,
positioner: this.config.positioner,
eventObject: i,
eventOn: this.config.eventOn,
eventOff: this.config.eventOff
};
this.data.popupId = this.popupManager.registerPopup(
this.id,
a,
e
), this.popupManager.setStatus(this.data.popupId, "isOpen", !1), this.autoOpen && this.show({ positioner: this.config.positioner });
}
},
mounted() {
this.$nextTick(() => this.startUp(1));
},
beforeUnmount() {
this.popupManager.destroyPopup(this.data.popupId), this.closeHandle();
}
}), E = (t, e) => {
const i = t.__vccOpts || t;
for (const [s, a] of e)
i[s] = a;
return i;
}, F = ["id"], V = ["width", "height", "viewBox"], L = ["fill", "d"];
function J(t, e, i, s, a, l) {
return u(), $(D, { to: "body" }, [
t.config.isPositioned || t.data.isDisplayed ? (u(), k("div", {
key: 0,
class: H(["gw-popup", t.config.class])
}, [
y("div", {
class: "popupContainer",
id: t.data.popupId
}, [
y("div", {
class: "popupElements",
style: x(t.styles.element)
}, [
t.data.svgParams !== null ? (u(!0), k(q, { key: 0 }, B(t.data.svgParams.paths, (h, n) => (u(), k("svg", {
key: h.class,
"xml:space": "preserve",
width: t.data.svgParams.width,
height: t.data.svgParams.height,
viewBox: t.data.svgParams.viewbox
}, [
y("path", {
class: H(["popupPath", h.class]),
fill: t.getFill(n),
d: h.path
}, null, 10, L)
], 8, V))), 128)) : R("", !0),
t.config.isModal && t.data.isDisplayed ? (u(), k("div", {
key: 1,
class: "modalBackground",
onClick: e[0] || (e[0] = (h) => t.forceHide())
})) : R("", !0),
y("div", {
class: "popupContent",
ref: "popupContent",
style: x(t.styles.content)
}, [
j(t.$slots, "default", { ref: "content" })
], 4)
], 4)
], 8, F)
], 2)) : R("", !0)
]);
}
const G = /* @__PURE__ */ E(_, [["render", J]]), Q = S({
name: "ToolTip",
components: {
PopUp: G
},
setup() {
const t = {
toolTipText: "",
ready: !1,
tips: [],
tipIndex: {}
}, e = [], i = C("popupManager");
return {
data: P(t),
callbacks: P(e),
popupManager: i
};
},
methods: {
registerTooltips(t) {
t.forEach((e) => {
const i = this.data.tipIndex[e.refName] ? this.data.tipIndex[e.refName] : this.data.tips.length;
this.data.tipIndex[e.refName] = i;
const s = {
refName: e.refName,
ref: e.ref,
text: e.text,
direction: e.direction
};
this.data.tips[i] = s;
const a = w(s.ref, "mouseenter", () => {
this.show(s.refName);
});
this.callbacks[i] = a;
const l = w(s.ref, "mouseleave", () => {
this.hide();
});
this.callbacks[i] = l;
}), this.data.ready = !0;
},
show(t) {
const e = this.data.tips[this.data.tipIndex[t]];
this.data.toolTipText = e.text, this.popupManager.showPopup(this.id, e.ref, e.direction);
},
hide() {
this.popupManager.hidePopup(this.id);
}
},
props: {
id: {
type: String,
default: "ToolTip"
},
cssClass: {
type: String,
default: "toolTip"
}
},
mounted() {
this.popupManager.connectToolTip(this.id, this.registerTooltips);
},
beforeUnmount() {
this.popupManager.disconnectToolTip(this.id), this.callbacks.forEach((t) => {
t();
}), this.callbacks = [];
}
});
function Z(t, e, i, s, a, l) {
const h = A("PopUp");
return u(), $(h, {
mode: "tooltip",
props: { isManual: !0 },
id: t.id
}, {
default: N(() => [
y("div", {
class: H(t.cssClass)
}, z(t.data.toolTipText), 3)
]),
_: 1
}, 8, ["id"]);
}
const nt = /* @__PURE__ */ E(Q, [["render", Z]]), X = {
log: !1,
debugMode: !1,
modes: {
dialog: {
props: {
isPositioned: !1,
class: "centered",
paddingX: 10,
paddingY: 10,
arrowOffset: 6,
logging: !1,
isManual: !0,
isModal: !0,
direction: "e"
}
},
tooltip: {
props: {
isManual: !1,
direction: "e"
},
directionMap: {
n: {
yReverse: !0,
switchXY: !1,
svgElements: ["arrowBox"],
startOffset: "H"
},
e: {
yReverse: !1,
switchXY: !0,
svgElements: ["arrowBox"],
startOffset: "0"
},
w: {
yReverse: !0,
switchXY: !0,
svgElements: ["arrowBox"],
startOffset: "W"
},
s: {
yReverse: !1,
switchXY: !1,
svgElements: ["arrowBox"],
startOffset: "0"
}
}
},
popup: {
props: {
isManual: !0,
logging: !1,
isModal: !0,
direction: "e"
}
},
windowPopup: {
props: {
isPositioned: !1,
class: "centered",
isManual: !0,
logging: !0,
isModal: !0
}
}
},
svgElements: {
arrowBox: {
fill: "backgroundColor",
pathTemplate: `
M {{T}},{{S}}
l {{AW/2}},{{AH}}
l {{W-T-R-AW/2}},0
q {{R}},0 {{R}},{{R}}
l 0,{{H-R-R-AH}}
q 0,{{R}} -{{R}},{{R}}
l -{{W-R-R}},0
q -{{R}},0 -{{R}},-{{R}}
l 0,-{{H-R-R-AH}}
q 0,-{{R}} {{R}},-{{R}}
l {{W-T-AW/2-R}},0
z
`
}
}
};
let Y = { ...X };
function K(t) {
Y = { ...X, ...t };
}
function tt() {
return Y;
}
const et = I("popupManager", {
state: () => ({
popupIndex: 0,
status: {},
callbacks: {},
callbackHandles: {},
settings: null,
toolTips: {},
globalConfig: tt()
// Use default config
}),
actions: {
mergeConfig(t, e, i) {
try {
const s = this.globalConfig.modes[t].props;
if (typeof s > "u")
return console.error("Popup Service handled non-existent mode key: " + t), e;
e = this.mergeTool(e, s), typeof i < "u" && (e = this.mergeTool(e, i));
} catch {
console.error(
"Unexpected error in PopupManager mergeConfig for: " + t
);
}
return e;
},
mergeTool(t, e) {
return Object.keys(e).forEach((s) => {
const a = e[s];
typeof a < "u" && this.setPropValue(s, t, a);
}), t;
},
setPropValue(t, e, i) {
e[t] = i;
},
connectToolTip(t, e) {
this.toolTips[t] = e;
},
registerToolTips(t, e, i = 5) {
this.toolTips[t] ? this.toolTips[t](e) : i > 0 ? setTimeout(() => this.registerToolTips(t, e, i - 1), 100) : console.error(`Tooltip handler for id '${t}' not found`);
},
disconnectToolTip(t) {
delete this.toolTips[t];
},
config(t) {
this.settings = t, K(t);
},
buildArrowBox(t) {
const e = {
width: t.width + "px",
height: t.height + "px",
viewbox: `0 0 ${t.width} ${t.height}`,
paths: []
}, i = this.globalConfig.modes[t.mode].directionMap;
if (!i) return null;
const s = i[t.direction];
return s ? (s.svgElements.forEach((a) => {
const l = this.globalConfig.svgElements[a], h = s.switchXY ? t.height : t.width, n = s.switchXY ? t.width : t.height, g = {
H: t.height,
W: t.width,
"-H": -t.height,
"-W": -t.width,
0: 0
}[s.startOffset] ?? 0, m = s.switchXY ? t.arrowWidth : t.arrowHeight, c = s.switchXY ? t.arrowHeight : t.arrowWidth, v = {
T: t.arrowPosition,
S: g,
W: h,
H: n,
AW: c,
AH: m,
R: t.cornerRadius,
RefX: t.refX,
RefY: t.refY,
RefW: t.refWidth,
RefH: t.refHeight
}, p = this.processSvgTemplate(
l.pathTemplate,
v,
s
);
e.paths.push({
class: a,
path: p,
fill: l.fill
});
}), e) : (console.error(
"Popup.vue expected to find a direction map item for:" + t.direction
), null);
},
processSvgTemplate(t, e, i) {
const a = t.replace(
/\{\{(.*?)\}\}/g,
(l, h) => {
try {
return String(
Function(
...Object.keys(e),
`return ${h}`
)(...Object.values(e))
);
} catch (n) {
return console.error(`Error processing template expression: ${h}`, n), "NaN";
}
}
).trim().split(`
`).map((l) => l.trim());
return i.yReverse && a.forEach((l, h) => {
const n = l.split(/[ ,]/);
if (n[0] !== "M")
for (let o = 2; o < n.length; o += 2)
n[o] = "," + String(-Number(n[o]));
a[h] = n.join(" ").split(" ,").join(",");
}), i.switchXY && a.forEach((l, h) => {
const n = l.split(/[ ,]/);
for (let o = 1; o < n.length; o += 2)
[n[o], n[o + 1]] = [n[o + 1], "," + n[o]];
a[h] = n.join(" ").split(" ,").join(",");
}), a.join(" ");
},
percentHeightWidth(t) {
return {
height: t * window.innerWidth / 100,
width: t * window.innerHeight / 100
};
},
log(t, e) {
var i;
(i = this.settings) != null && i.log && console.log(t + ":" + JSON.stringify(e, null, " "));
},
logSimple(t) {
var e;
(e = this.settings) != null && e.log && console.log(t);
},
attachEvents(t, e) {
const i = this.callbackHandles[t].show;
i !== null && i(), this.logSimple("attachEvent: show");
const s = w(
e.eventObject,
e.eventOn,
() => {
this.callbacks[t].show({ positioner: e.positioner });
const a = this.callbackHandles[t].hide;
a !== null && a(), this.logSimple("attachEvent: hide");
const l = w(
e.eventObject,
e.eventOff,
() => {
this.callbacks[t].hide({ positioner: "" });
}
);
this.callbackHandles[t].hide = l;
}
);
this.callbackHandles[t].show = s;
},
attachEvent(t, e, i, s) {
const a = this.callbackHandles[t][s];
a !== null && i && a(), this.logSimple("attachEvent: " + s);
const l = w(e, i, () => {
this.callbacks[t][s]({});
});
this.callbackHandles[t][s] = l;
},
registerPopup(t, e, i) {
return t || (t = "popup_" + this.popupIndex++), this.callbacks[t] && (this.logSimple("duplicate popup register, removing prior callback"), this.destroyPopup(t), this.logSimple("Popup " + t + " was re-registered")), this.callbacks[t] = i, this.callbackHandles[t] = { show: null, hide: null, refresh: null }, e.eventObject && !e.isManual && e.positioner && this.attachEvents(t, e), this.attachEvent(t, window, "resize", "refresh"), this.status[t] = {
isTooltip: e.isTooltip,
isOpen: !1
}, this.logSimple("registered popup: " + t), t;
},
destroyPopup(t) {
this.logSimple("Destroyed pop up: " + t);
try {
this.callbackHandles[t].hide !== null && this.callbackHandles[t].hide(), this.callbackHandles[t].show !== null && this.callbackHandles[t].show(), this.callbackHandles[t].refresh !== null && this.callbackHandles[t].refresh(), delete this.callbacks[t], delete this.callbackHandles[t], delete this.status[t], this.popupIndex = 0;
} catch {
console.error("Error destroying popup " + t);
}
},
closeOpenTooltips(t) {
Object.keys(this.status).forEach((e) => {
e !== t && this.status[e].isOpen && this.status[e].isTooltip && (this.logSimple("Tooltip: " + e + " autoclosed"), this.callbacks[e].hide({}), this.logSimple("autoclosed: " + e), this.status[e].isOpen = !1);
});
},
showPopup(t, e, i) {
setTimeout(() => {
this.callbacks[t] ? (this.callbacks[t].show({
positioner: e || "",
direction: i
}), this.status[t].isOpen = !0) : console.log("showPopup: Popup " + t + " was not found");
}, 10);
},
hidePopup(t) {
this.callbacks[t] ? (this.callbacks[t].hide({}), this.status[t].isOpen = !1) : this.logSimple("hidePopup: Popup " + t + " was not found");
},
refreshPopup(t) {
const e = this.callbacks[t];
e && setTimeout(() => e.refresh({}), 100);
},
setStatus(t, e, i) {
this.status[t] || (this.status[t] = { isOpen: !1, isTooltip: !0 }, this.logSimple(
"PopupManager.setStatus:" + t + "," + e + "," + i
)), this.status[t][e] = i;
},
reset() {
this.callbacks = {}, this.callbackHandles = {}, this.status = {}, this.popupIndex = 0;
}
}
}), at = {
install(t) {
const e = U();
t.use(e);
const i = et(e);
t.provide("popupManager", i);
}
};
export {
G as PopUp,
at as PopupPlugin,
nt as ToolTip,
et as usePopupManager
};