hana-img-viewer
Version:
A lightweight and easy-to-use image previewer for Vue 3
497 lines (496 loc) • 17 kB
JavaScript
import { ref as d, computed as m, watch as _, onMounted as O, onUnmounted as U, watchEffect as P, onWatcherCleanup as ce, defineComponent as ve, shallowRef as fe, onBeforeUnmount as pe, createElementBlock as A, openBlock as N, Fragment as me, createBlock as G, createCommentVNode as D, createElementVNode as j, Teleport as K, normalizeStyle as J, unref as Q, mergeProps as he, toHandlers as we, toDisplayString as ge } from "vue";
function ee(e, n) {
const t = n[0] - e[0], o = n[1] - e[1];
return Math.hypot(t, o);
}
function ye(e) {
let n = 0;
if (e) {
let o = e;
for (; o && o !== document.body; ) {
const a = window.getComputedStyle(o).zIndex;
if (a !== "auto" && a !== "") {
const l = Number.parseInt(a, 10);
Number.isNaN(l) || (n = Math.max(n, l));
}
o = o.parentElement;
}
}
const t = document.querySelectorAll("*");
for (const o of t) {
const a = window.getComputedStyle(o).zIndex;
if (a !== "auto" && a !== "") {
const l = Number.parseInt(a, 10);
Number.isNaN(l) || (n = Math.max(n, l));
}
}
return Math.max(n + 1, 9999);
}
function xe(e) {
if (!e)
return !1;
let n = e;
for (; n && n !== document.body; ) {
const t = window.getComputedStyle(n), o = n.classList;
if (t.position === "fixed" || o.contains("el-dialog") || o.contains("ant-modal") || o.contains("modal") || o.contains("dialog") || n.hasAttribute("role") && (n.getAttribute("role") === "dialog" || n.getAttribute("role") === "modal"))
return !0;
n = n.parentElement;
}
return !1;
}
function be(e) {
let n = e.parentElement;
for (; n && n !== document.body && n !== document.documentElement; ) {
if (window.getComputedStyle(n).position === "fixed")
return !0;
n = n.parentElement;
}
return !1;
}
function Ee(e, n, t) {
const o = e.getBoundingClientRect();
return be(e) ? {
top: o.top,
left: o.left
} : {
top: o.top + t,
left: o.left + n
};
}
function te(e) {
if (!e.value)
return [0, 0];
const n = window.getComputedStyle(e.value).transform;
if (n === "none")
return [0, 0];
const t = n.match(/matrix\((.+)\)/);
if (!t)
return [0, 0];
const o = t[1].split(", ");
return [
Number.parseFloat(o[4]) || 0,
Number.parseFloat(o[5]) || 0
];
}
function B(e) {
typeof e > "u" || typeof e.preventDefault != "function" || e.preventDefault();
}
function Ie(e, n) {
Object.assign(e.style, n);
}
function Le(e) {
const { imgRef: n, props: t } = e, o = d(t.previewZIndex), a = d(!1), l = m(() => t.autoZIndex ? o.value : t.previewZIndex), h = () => {
if (!(!t.autoZIndex || !n.value))
if (a.value = xe(n.value), a.value) {
const s = ye(n.value);
o.value = Math.max(s, t.previewZIndex);
} else
o.value = t.previewZIndex;
};
return _(n, () => {
h();
}, { immediate: !0 }), _(() => [t.autoZIndex, t.previewZIndex], () => {
h();
}), {
finalZIndex: l,
isInModal: a,
updateZIndex: h
};
}
function Se(e, n, t) {
var o = t || {}, a = o.noTrailing, l = a === void 0 ? !1 : a, h = o.noLeading, s = h === void 0 ? !1 : h, x = o.debounceMode, v = x === void 0 ? void 0 : x, w, f = !1, p = 0;
function b() {
w && clearTimeout(w);
}
function T(S) {
var E = S || {}, c = E.upcomingOnly, k = c === void 0 ? !1 : c;
b(), f = !k;
}
function C() {
for (var S = arguments.length, E = new Array(S), c = 0; c < S; c++)
E[c] = arguments[c];
var k = this, g = Date.now() - p;
if (f)
return;
function L() {
p = Date.now(), n.apply(k, E);
}
function M() {
w = void 0;
}
!s && v && !w && L(), b(), v === void 0 && g > e ? s ? (p = Date.now(), l || (w = setTimeout(v ? M : L, e))) : L() : l !== !0 && (w = setTimeout(v ? M : L, v === void 0 ? e - g : e));
}
return C.cancel = T, C;
}
function Me(e, n = {}) {
const t = d(null);
let o = null;
const a = () => {
if (!e.value)
return;
const s = e.value.getBoundingClientRect();
t.value = s, n.onChange?.(s);
}, l = n.throttle ? Se(n.throttleDelay ?? 100, a, { noTrailing: !1 }) : a, h = () => {
o?.disconnect(), o = null, window.removeEventListener("resize", l);
};
return O(() => {
if (e.value && (a(), typeof ResizeObserver < "u" && (o = new ResizeObserver(l), o.observe(e.value)), window.addEventListener("resize", l), e.value.tagName === "IMG")) {
const s = e.value;
s.complete || s.addEventListener("load", a, { once: !0 });
}
}), U(h), P(() => {
if (h(), !!e.value) {
if (a(), typeof ResizeObserver < "u" && (o = new ResizeObserver(l), o.observe(e.value)), window.addEventListener("resize", l), e.value.tagName === "IMG") {
const s = e.value;
s.complete || s.addEventListener("load", a, { once: !0 });
}
ce(h);
}
}), {
rect: t
};
}
function ke(e) {
const { handleWheel: n, handleTouchStart: t, handleKeyDown: o } = e;
function a(l) {
if (!(typeof window > "u"))
switch (l) {
case "on":
window.addEventListener("wheel", B, { passive: !1 }), window.addEventListener("touchmove", B, { passive: !1 }), window.addEventListener("wheel", n), window.addEventListener("touchstart", t), window.addEventListener("keydown", o);
break;
case "off":
window.removeEventListener("wheel", B), window.removeEventListener("touchmove", B), window.removeEventListener("wheel", n), window.removeEventListener("touchstart", t), window.removeEventListener("keydown", o);
break;
}
}
return {
toggleEventListener: a
};
}
function Te(e, n) {
const { zoomStep: t, zoomMax: o, zoomMin: a, dblClickZoomTo: l } = n, h = d(!1), s = d(0), x = d(0), v = d(1);
let w = 0, f = 0, p = 0, b = 0, T = 0;
P(() => {
e.value && (e.value.style.cursor = h.value ? "grabbing" : "grab");
});
const C = m(() => {
const u = s.value === 0 ? "-50%" : `${s.value}px`, r = x.value === 0 ? "-50%" : `${x.value}px`;
return `translate(${u}, ${r}) scale(${v.value})`;
}), S = () => {
e.value && Ie(e.value, { transform: C.value });
}, E = (u = 0, r) => {
r !== void 0 ? v.value = r : v.value += u, v.value = Math.max(a, Math.min(o, v.value)), S();
}, c = (u) => {
E(u.deltaY < 0 ? t : -t);
}, k = () => {
E(0, v.value > 1 ? 1 : l);
}, g = (u) => {
if (e.value) {
if (u.touches.length === 2) {
const [r, y] = [u.touches[0], u.touches[1]], Z = ee(
[r.pageX, r.pageY],
[y.pageX, y.pageY]
), W = Z / w;
E(0, v.value * W), w = Z;
} else if (u.touches.length === 1) {
const r = u.touches[0], y = r.pageX - f, Z = r.pageY - p;
s.value = b + y, x.value = T + Z, S();
}
}
}, L = () => {
document.removeEventListener("touchmove", g), document.removeEventListener("touchend", L);
}, M = (u) => {
if (e.value) {
if (u.touches.length === 2) {
const [r, y] = [u.touches[0], u.touches[1]];
w = ee(
[r.pageX, r.pageY],
[y.pageX, y.pageY]
);
} else if (u.touches.length === 1) {
const r = u.touches[0];
f = r.pageX, p = r.pageY;
const y = te(e);
b = y[0], T = y[1];
}
document.addEventListener("touchmove", g), document.addEventListener("touchend", L);
}
}, $ = (u) => {
if (!e.value)
return;
const r = u.clientX - f, y = u.clientY - p;
s.value = b + r, x.value = T + y, S();
}, Y = () => {
h.value = !1, document.removeEventListener("mousemove", $), document.removeEventListener("mouseup", Y);
}, H = (u) => {
if (!e.value)
return;
h.value = !0, f = u.clientX, p = u.clientY;
const r = te(e);
b = r[0], T = r[1], document.addEventListener("mousemove", $), document.addEventListener("mouseup", Y);
}, X = () => {
s.value = 0, x.value = 0, v.value = 1;
}, z = () => {
document.removeEventListener("wheel", c), document.removeEventListener("touchstart", M), document.removeEventListener("touchmove", g), document.removeEventListener("touchend", L), document.removeEventListener("mousemove", $), document.removeEventListener("mouseup", Y);
};
return U(z), {
handleWheel: c,
handleTouchStart: M,
handleDblclick: k,
handleMouseDown: H,
initTransformer: X,
cleanupListeners: z
};
}
function Ce() {
const e = d(typeof window < "u" ? window.innerWidth : 0), n = d(typeof window < "u" ? window.innerHeight : 0), t = d(typeof window < "u" ? window.scrollX : 0), o = d(typeof window < "u" ? window.scrollY : 0), a = () => {
typeof window > "u" || (e.value = window.innerWidth, n.value = window.innerHeight);
}, l = () => {
typeof window > "u" || (t.value = window.scrollX || window.pageXOffset, o.value = window.scrollY || window.pageYOffset);
};
return O(() => {
a(), l(), window.addEventListener("resize", a, { passive: !0 }), window.addEventListener("scroll", l, { passive: !0 });
}), U(() => {
typeof window > "u" || (window.removeEventListener("resize", a), window.removeEventListener("scroll", l));
}), {
width: e,
height: n,
scrollX: t,
scrollY: o
};
}
const I = {
duration: 500,
maskBgColor: "black",
maskOpacity: 0.1,
previewZIndex: 9999,
autoZIndex: !0,
previewMaxWidth: "80vw",
previewMaxHeight: "80vh",
zoomStep: 0.2,
zoomMin: 0.2,
zoomMax: 10,
dblClickZoom: !0,
dblClickZoomTo: 2
}, ze = {
/* ===== 支持v-model绑定 ===== */
displaying: { type: Boolean, default: void 0 },
applyingPreviewStyles: { type: Boolean, default: void 0 },
isAnimating: { type: Boolean, default: void 0 },
src: { type: String, required: !0 },
alt: String,
width: [String, Number],
height: [String, Number],
duration: { type: Number, default: I.duration },
maskBgColor: { type: String, default: I.maskBgColor },
maskOpacity: { type: Number, default: I.maskOpacity },
previewZIndex: { type: Number, default: I.previewZIndex },
autoZIndex: { type: Boolean, default: I.autoZIndex },
previewMaxWidth: {
type: [String, Number],
default: I.previewMaxWidth
},
previewMaxHeight: {
type: [String, Number],
default: I.previewMaxHeight
},
zoomStep: { type: Number, default: I.zoomStep },
zoomMin: { type: Number, default: I.zoomMin },
zoomMax: { type: Number, default: I.zoomMax },
dblClickZoom: { type: Boolean, default: I.dblClickZoom },
dblClickZoomTo: {
type: Number,
default: I.dblClickZoomTo
}
}, Ze = {
"update:displaying": (e) => !0,
"update:applyingPreviewStyles": (e) => !0,
"update:isAnimating": (e) => !0,
displayChange: (e) => !0,
previewStylesChange: (e) => !0,
animatingChange: (e) => !0
}, Ne = ["src"], _e = { style: { display: "flex", "flex-direction": "column", "align-items": "center", gap: "0.5rem" } }, $e = ["src", "alt"], Ye = {
key: 0,
style: { "font-size": "0.8rem", color: "#666" }
}, De = /* @__PURE__ */ ve({
name: "HanaImgViewer",
__name: "HanaImgViewer",
props: ze,
emits: Ze,
setup(e, { emit: n }) {
const t = e, o = n, a = d(!1);
O(() => {
a.value = !0;
});
const l = d(null), h = d(null), s = d(null), x = d(!1), v = d(!1), w = d(!1), f = m({
get: () => t.displaying !== void 0 ? t.displaying : x.value,
set: (i) => {
t.displaying !== void 0 ? o("update:displaying", i) : x.value = i, o("displayChange", i);
}
}), p = m({
get: () => t.applyingPreviewStyles !== void 0 ? t.applyingPreviewStyles : v.value,
set: (i) => {
t.applyingPreviewStyles !== void 0 ? o("update:applyingPreviewStyles", i) : v.value = i, o("previewStylesChange", i);
}
}), b = m({
get: () => t.isAnimating !== void 0 ? t.isAnimating : w.value,
set: (i) => {
t.isAnimating !== void 0 ? o("update:isAnimating", i) : w.value = i, o("animatingChange", i);
}
});
_(() => t.displaying, (i) => {
i !== void 0 && i !== x.value && (x.value = i);
}), _(() => t.applyingPreviewStyles, (i) => {
i !== void 0 && i !== v.value && (v.value = i);
}), _(() => t.isAnimating, (i) => {
i !== void 0 && i !== w.value && (w.value = i);
});
function T() {
b.value = !0, setTimeout(() => {
b.value = !1;
}, t.duration);
}
const C = d(0), S = m(() => ({
width: (typeof t.width == "number" ? `${t.width}px` : t.width) ?? "fit-content",
height: (typeof t.height == "number" ? `${t.height}px` : t.height) ?? "fit-content",
visibility: f.value ? "hidden" : "visible"
}));
function E() {
b.value || (f.value || C.value++, T(), f.value ? (p.value = !1, setTimeout(() => {
f.value = !1;
}, t.duration)) : f.value = !0);
}
const c = fe(null);
O(() => {
c.value = Te(s, t);
}), pe(() => {
c.value?.cleanupListeners();
});
const k = d({}), { rect: g } = Me(l, {
throttle: !0,
throttleDelay: 100
}), { finalZIndex: L } = Le({
imgRef: l,
props: t
}), M = m(() => g.value ? g.value.width / g.value.height : 0), { width: $, height: Y, scrollX: H, scrollY: X } = Ce(), z = m(() => $.value / Y.value), u = m(() => `${t.duration}ms`), r = m(
() => g.value && M.value > z.value ? `${g.value.width}px` : "auto"
), y = m(
() => g.value ? M.value > z.value ? "auto" : `${g.value.height}px` : "auto"
);
function Z() {
return l.value ? Ee(l.value, H.value, X.value) : { top: 0, left: 0 };
}
const W = m(() => (C.value, `${Z().top}px`)), ne = m(() => (C.value, `${Z().left}px`)), oe = m(
() => M.value > z.value ? `${t.previewMaxWidth}` : "auto"
), ie = m(
() => M.value > z.value ? "auto" : `${t.previewMaxHeight}`
), ae = m(() => `calc(50vh + ${X.value}px)`);
function le(i) {
i.key === "Escape" && f.value && E();
}
const R = d({
toggleEventListener: (i) => {
}
});
P(() => {
c.value && (R.value = ke({
handleWheel: c.value.handleWheel,
handleTouchStart: c.value.handleTouchStart,
handleKeyDown: le
}));
});
const ue = m(() => ({
opacity: p.value ? t.maskOpacity : 0
})), re = m(() => ({
transition: b.value ? `all ${u.value}` : "none",
width: p.value ? oe.value : r.value,
height: p.value ? ie.value : y.value,
top: p.value ? ae.value : W.value,
left: p.value ? "50%" : ne.value,
transform: p.value ? "translate(-50%, -50%)" : "none"
}));
return P(() => {
c.value?.initTransformer(), f.value ? (typeof document < "u" && (document.body.style.overflow = "hidden"), requestAnimationFrame(() => {
p.value = !0;
})) : typeof document < "u" && (document.body.style.overflow = "auto");
}), _([f, b], ([i, F], [q, V]) => {
if (!c.value)
return;
const se = i && !F && q && V && p.value, de = i && F && q && !V;
se && (k.value = {
dblclick: c.value.handleDblclick,
mousedown: c.value.handleMouseDown
}, R.value.toggleEventListener("on")), de && (R.value.toggleEventListener("off"), k.value = {});
}), (i, F) => (N(), A(me, null, [
a.value ? (N(), G(K, {
key: 0,
to: "body"
}, [
f.value ? (N(), A("div", {
key: 0,
ref_key: "maskRef",
ref: h,
style: J({
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: i.maskBgColor,
zIndex: Q(L) - 1,
transition: `all ${u.value}`,
opacity: ue.value.opacity
}),
onClick: E
}, null, 4)) : D("", !0)
])) : D("", !0),
a.value ? (N(), G(K, {
key: 1,
to: "body"
}, [
f.value ? (N(), A("img", he({
key: 0,
ref_key: "previewerRef",
ref: s,
src: i.src,
draggable: "false",
style: {
position: "absolute",
objectFit: "cover",
cursor: "grab",
zIndex: Q(L),
...re.value
}
}, we(k.value, !0)), null, 16, Ne)) : D("", !0)
])) : D("", !0),
j("div", _e, [
j("div", {
style: J(S.value)
}, [
j("img", {
ref_key: "imgRef",
ref: l,
src: i.src,
alt: i.alt,
style: { width: "100%", height: "100%", "object-fit": "cover", cursor: "pointer" },
onClick: E
}, null, 8, $e)
], 4),
i.alt ? (N(), A("span", Ye, ge(i.alt), 1)) : D("", !0)
])
], 64));
}
}), Xe = [De];
function Ae(e) {
Xe.forEach((n) => {
e.component(n.name || "HanaImgViewer", n);
});
}
const Oe = { install: Ae };
export {
De as HanaImgViewer,
Oe as default,
I as defaultImgViewerProps,
Ze as imgViewerEmitsObj,
ze as imgViewerPropsObj
};