vue-grabbing-box
Version:
vue-grabbing-box is a container component that allows you to grab to change view-box and also with scaling
590 lines (589 loc) • 17.4 kB
JavaScript
const Y = "1.0.0";
function v() {
var t = navigator.userAgent, e = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
return !e.some(function(s) {
return t.includes(s);
});
}
function T(t) {
var e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 50, s = 0;
return function() {
if (typeof t != "function")
throw new Error("".concat(t, " is not a function"));
var a = this;
if (!(Date.now() - s < e)) {
s = Date.now();
for (var n = arguments.length, o = new Array(n), i = 0; i < n; i++)
o[i] = arguments[i];
return t.apply(a, o);
}
};
}
var $ = function() {
var t = this, e = t.$createElement, s = t._self._c || e;
return s("div", {
staticClass: "scale-btn-group flex items-center"
}, [s("div", {
staticClass: "scale-button btn-plus flex justify-center items-center",
class: {
"is-disabled": t.scaleLimitOverMax
},
on: {
click: function(a) {
return a.stopPropagation(), t.zoomIn.apply(null, arguments);
}
}
}, [s("svg", {
attrs: {
xmlns: "http://www.w3.org/2000/svg",
width: "20",
height: "20",
viewBox: "0 0 24 24"
}
}, [s("path", {
attrs: {
fill: "currentColor",
d: "M18 12.998h-5v5a1 1 0 0 1-2 0v-5H6a1 1 0 0 1 0-2h5v-5a1 1 0 0 1 2 0v5h5a1 1 0 0 1 0 2z"
}
})])]), s("div", {
staticStyle: {
display: "inline-block",
margin: "0 8px"
}
}, [t._v(t._s(t.scaling) + "%")]), s("div", {
staticClass: "scale-button btn-minus flex justify-center items-center",
class: {
"is-disabled": t.scaleLimitBelowMin
},
on: {
click: function(a) {
return a.stopPropagation(), t.zoomOut.apply(null, arguments);
}
}
}, [s("svg", {
attrs: {
xmlns: "http://www.w3.org/2000/svg",
width: "20",
height: "20",
viewBox: "0 0 24 24"
}
}, [s("path", {
attrs: {
fill: "currentColor",
d: "M18 12.998H6a1 1 0 0 1 0-2h12a1 1 0 0 1 0 2z"
}
})])]), s("div", {
staticClass: "scale-button btn-reset",
on: {
click: function(a) {
return a.stopPropagation(), t.reset.apply(null, arguments);
}
}
}, [t._v("重置")])]);
}, w = [];
function y(t, e, s, a, n, o, i, f) {
var r = typeof t == "function" ? t.options : t;
e && (r.render = e, r.staticRenderFns = s, r._compiled = !0), a && (r.functional = !0), o && (r._scopeId = "data-v-" + o);
var c;
if (i ? (c = function(l) {
l = l || // cached call
this.$vnode && this.$vnode.ssrContext || // stateful
this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext, !l && typeof __VUE_SSR_CONTEXT__ < "u" && (l = __VUE_SSR_CONTEXT__), n && n.call(this, l), l && l._registeredComponents && l._registeredComponents.add(i);
}, r._ssrRegister = c) : n && (c = f ? function() {
n.call(
this,
(r.functional ? this.parent : this).$root.$options.shadowRoot
);
} : n), c)
if (r.functional) {
r._injectStyles = c;
var x = r.render;
r.render = function(D, p) {
return c.call(p), x(D, p);
};
} else {
var g = r.beforeCreate;
r.beforeCreate = g ? [].concat(g, c) : [c];
}
return {
exports: t,
options: r
};
}
const B = {
name: "ScaleButtonGroup",
props: {
// 当前缩放值
scaling: {
type: Number,
default: 100
},
// scale最大百分比
maxScale: {
type: Number,
default: 200
},
// scale最小百分比
minScale: {
type: Number,
default: 20
}
},
computed: {
scaleLimitOverMax() {
return this.scaling >= this.maxScale;
},
scaleLimitBelowMin() {
return this.scaling <= this.minScale;
}
},
methods: {
zoomIn() {
this.$emit("zoom-in");
},
zoomOut() {
this.$emit("zoom-out");
},
reset() {
this.$emit("reset");
}
}
}, S = {};
var P = /* @__PURE__ */ y(
B,
$,
w,
!1,
E,
"0123c1b8",
null,
null
);
function E(t) {
for (let e in S)
this[e] = S[e];
}
const C = /* @__PURE__ */ function() {
return P.exports;
}();
var M = function() {
var t = this, e = t.$createElement, s = t._self._c || e;
return s("div", {
ref: "containerRef",
staticClass: "grabbing-box-container w-full h-full overflow-hidden flex flex-col relative",
class: {
"cursor-grab": !t.dragging,
"cursor-grabbing": t.dragging
}
}, [t.scaleButtons ? s("scale-button-group", {
staticClass: "scale-button-group",
class: {
"custom-position-top": t.safeScaleButtonsPosition.top,
"custom-position-bottom": t.safeScaleButtonsPosition.bottom,
"custom-position-left": t.safeScaleButtonsPosition.left,
"custom-position-right": t.safeScaleButtonsPosition.right
},
style: `
--scale-buttons-space-x: ${t.scaleButtonsSpaceX};
--scale-buttons-space-y: ${t.scaleButtonsSpaceY};
`,
attrs: {
scaling: t.scaling
},
on: {
"zoom-in": t.zoomIn,
"zoom-out": t.zoomOut,
reset: t.reset
}
}) : t._e(), s("div", {
ref: "contentBoxRef",
staticClass: "grabbing-box w-full h-full",
staticStyle: {
"transform-origin": "center center",
"user-select": "none",
transform: "matrix(1, 0, 0, 1, 0, 0)"
}
}, [t._t("default")], 2)], 1);
}, R = [];
const u = 1, d = 6, _ = 1, X = {
name: "GrabbingBox",
props: {
useCapture: {
type: Boolean,
default: !0
},
// scale最大百分比
maxScale: {
type: Number,
default: 200
},
// scale最小百分比
minScale: {
type: Number,
default: 20
},
// 初始化为指定比例
initScale: {
type: Number,
default: 100
},
// 是否显示缩放按钮组
scaleButtons: {
type: Boolean,
default: !0
},
// Scale buttons position
scaleButtonsPosition: {
type: String,
default: "top right"
},
scaleButtonsSpaceX: {
type: String,
default: "10px"
},
scaleButtonsSpaceY: {
type: String,
default: "20px"
},
// 点击缩放按钮时的步长
scaleStep: {
type: Number,
default: 10
},
// 节流函数的时间间隔
throttleSpan: {
type: Number,
default: 50
},
/**
* @deprecated
*/
scrollSpeed: {
type: Number,
default: _,
validator(t) {
return u <= t && t <= d;
}
},
/**
* [#7](https://github.com/CoderMonkie/vue-grabbing-box/issues/7)
* 阻止 drag 时的 click 事件
*
* 默认当 drag 后不触发 click 事件
* 如有特殊需要,将`emitClickOnDrag`设为`true`
*/
emitClickOnDrag: {
type: Boolean,
default: !1
}
},
components: { ScaleButtonGroup: C },
mounted() {
this.init();
},
beforeDestroy() {
Object.entries(this.getContainerEvents()).forEach((t) => {
const [e, s] = t;
this.unbindEvent(this.$refs.containerRef, e, s);
}), Object.entries(this.getBodyEvents()).forEach((t) => {
const [e, s] = t;
this.unbindEvent(document, e, s);
});
},
computed: {
/**
* 显示用缩放百分比
*/
scaling() {
const { scale: t } = this.lastTransformData;
return parseInt(t * 1e4 / 100);
},
safeScrollSpeed() {
return isNaN(this.scrollSpeed) ? _ : u > this.scrollSpeed ? u : d < this.scrollSpeed ? d : this.scrollSpeed;
},
safeScaleButtonsPosition() {
const t = (this.scaleButtonsPosition || "").trim().toLowerCase();
if (!t)
return { right: !0, top: !0, left: !1, bottom: !1 };
const e = t.includes("left") ? "left" : "right", s = t.includes("bottom") ? "bottom" : "top";
return {
top: s === "top",
bottom: s === "bottom",
left: e === "left",
right: e === "right"
};
}
},
data() {
return {
lastTransformData: {
scale: 1,
translateX: 0,
translateY: 0
},
lastPosition: {
x: null,
y: null,
distance: 0
// 两指触点之间的距离
},
readyToDrag: !1,
dragging: !1,
readyToScale: !1,
isScaling: !1,
// 缩放导致偏移的比例
scaleTranslateProportion: [0, 0],
// TODO
dragged: !1
// fix #7 完成一次拖拽后标记为true,控制 click 事件是否取消冒泡等,适时重置
};
},
methods: {
/** 初始化为指定比例 */
useInitSacle() {
if (this.maxScale < this.minScale)
throw new Error("[Props Error]maxScale should greater than minSacle");
let t = this.initScale;
t > this.maxScale ? t = this.maxScale : t < this.minScale && (t = this.minScale), this.lastTransformData.scale = this.ensureScaleInRange(
t / 100
), this.setMatrix(this.$refs.contentBoxRef);
},
init() {
this.useInitSacle();
const t = this.getBodyEvents();
Object.entries(t).forEach((a) => {
const [n, o] = a;
this.bindEvent(document, n, o, {
passive: !1,
capture: !1
});
});
const e = this.getContainerEvents(), s = Object.keys(t);
Object.entries(e).forEach((a) => {
const [n, o] = a, i = s.includes(n) ? o : T(o, this.throttleSpan);
this.bindEvent(this.$refs.containerRef, n, i, {
capture: this.useCapture,
passive: !1
});
});
},
/**
* 需要阻止默认行为的事件
* 如移动端的时候需要禁掉浏览器默认的动作识别(如下拉刷新、右划返回等)
* PC端的时候取消 ctrl 加滚轮的默认行为
* 应注意:
* - passive 应设 false
* - 全都应处理掉,不能使用节流函数
*/
getBodyEvents() {
return v() ? {
wheel: (t) => {
t.ctrlKey && this.disableDefaultBehavior(t);
}
} : {
touchmove: this.disableDefaultBehavior
};
},
/**
* 组件内要处理的事件
*/
getContainerEvents() {
return v() ? {
mousedown: this.onMouseDown,
mousemove: this.onMouseMove,
mouseup: this.onMouseUp,
mouseleave: this.onMouseLeave,
wheel: this.onWheel
} : {
touchstart: this.onTouchStart,
touchmove: this.onTouchMove,
touchend: this.onTouchEnd
};
},
/**
* 阻止移动端浏览器的默认滑动(如下拉刷新与左划回退)
*/
disableDefaultBehavior(t) {
t.preventDefault();
},
reset() {
this.lastTransformData = {
scale: 1,
translateX: 0,
translateY: 0
}, this.setMatrix(this.$refs.contentBoxRef), this.$emit("reset");
},
zoomIn() {
const { scale: t } = this.lastTransformData, e = (t * 100 + this.scaleStep) / 100;
this.lastTransformData.scale = Math.min(e, this.maxScale / 100), this.setMatrix(this.$refs.contentBoxRef), this.$emit("zoom-in", this.lastTransformData);
},
zoomOut() {
const { scale: t } = this.lastTransformData, e = (t * 100 - this.scaleStep) / 100;
this.lastTransformData.scale = Math.max(e, this.minScale / 100), this.setMatrix(this.$refs.contentBoxRef), this.$emit("zoom-out", this.lastTransformData);
},
bindEvent(t, e, s, a) {
t.addEventListener(e, s, a);
},
unbindEvent(t, e, s) {
t.removeEventListener(e, s);
},
move(t, e) {
const { translateX: s, translateY: a } = this.lastTransformData, { deltaX: n, deltaY: o } = e, [i, f] = [s + n, a + o];
this.lastTransformData = {
...this.lastTransformData,
translateX: i,
translateY: f
}, this.setMatrix(t), this.$emit("move", this.lastTransformData);
},
setMatrix(t) {
const { scale: e, translateX: s, translateY: a } = this.lastTransformData, n = this.ensureScaleInRange(e);
t.style.transform = `matrix(${n}, 0, 0, ${n}, ${s}, ${a})`, this.lastTransformData.scale = n, this.$emit("update", this.lastTransformData);
},
/**
* -------------------
* PC端事件处理
* -------------------
*/
handleInnerClick(t) {
!this.emitClickOnDrag && this.dragged && (t.stopPropagation(), t.preventDefault());
},
onMouseDown(t) {
this.readyToDrag = !0, this.lastPosition = {
x: t.clientX,
y: t.clientY
}, this.emitClickOnDrag || (this.dragged = !1, this.bindEvent(this.$refs.containerRef, "click", this.handleInnerClick, { capture: !0 }));
},
onMouseMove(t) {
if (this.readyToDrag) {
t.stopPropagation(), t.preventDefault(), this.dragging = !0;
const { clientX: e, clientY: s } = t, { x: a, y: n } = this.lastPosition, [o, i] = [e - a, s - n];
this.move(this.$refs.contentBoxRef, { deltaX: o, deltaY: i }), this.lastPosition = {
x: e,
y: s
}, this.emitClickOnDrag || (this.dragged = !0);
}
},
onMouseUp() {
this.readyToDrag = !1, this.dragging = !1, this.unbindEvent(this.$refs.containerRef, "click", this.handleInnerClick);
},
onMouseLeave() {
},
onWheel(t) {
const { ctrlKey: e, wheelDelta: s } = t;
if (t.preventDefault(), t.stopPropagation(), e)
s > 0 ? this.zoomIn() : this.zoomOut();
else {
const { deltaMode: a, wheelDeltaX: n, wheelDeltaY: o } = t;
a === 0 && (this.lastTransformData.translateX += n, this.lastTransformData.translateY += o), this.setMatrix(this.$refs.contentBoxRef);
}
},
/**
* -------------------
* 移动端事件适配
* -------------------
*/
/** touchstart */
onTouchStart(t) {
if (t.touches.length === 1) {
const { clientX: e, clientY: s } = t.touches[0];
this.lastPosition = {
x: e,
y: s
}, this.readyToDrag = !0, this.dragging = !1, this.readyToScale = !1;
} else if (t.touches.length === 2) {
this.readyToScale = !0, this.readyToDrag = !1, this.dragging = !1;
const [e, s] = t.touches, a = e.clientX - s.clientX, n = e.clientY - s.clientY;
this.lastPosition.distance = Math.sqrt(a * a + n * n);
const o = [
(e.clientX + a / 2 - this.lastTransformData.x) / this.lastTransformData.scale,
(e.clientY + n / 2 - this.lastTransformData.y) / this.lastTransformData.scale
];
this.scaleTranslateProportion = [
o[0] / this.$refs.contentBoxRef.offsetWidth,
o[1] / this.$refs.contentBoxRef.offsetHeight
];
}
},
/** touchmove */
onTouchMove(t) {
if (t.preventDefault(), t.stopPropagation(), t.touches.length === 1 && this.readyToDrag) {
this.dragging = !0;
const { clientX: e, clientY: s } = t.touches[0], { x: a, y: n } = this.lastPosition, [o, i] = [e - a, s - n];
this.move(this.$refs.contentBoxRef, { deltaX: o, deltaY: i }), this.lastPosition = {
...this.lastPosition,
x: e,
y: s
};
} else if (t.touches.length === 2) {
if (!this.readyToScale) {
this.onTouchStart(t);
return;
}
const [e, s] = t.touches, a = Math.sqrt((e.clientX - s.clientX) * (e.clientX - s.clientX) + (e.clientY - s.clientY) * (e.clientY - s.clientY));
this.getScaling(a / this.lastPosition.distance, !1), this.lastPosition.distance = a;
}
},
/** touchend */
onTouchEnd(t) {
this.readyToDrag = !1, this.dragging = !1, this.readyToScale = !1, this.isScaling = !1, this.lastPosition = {
x: 0,
y: 0,
distance: 0
};
},
// ---------------------------------------
// 进行指定大小的缩放
getScaling(t, e = !0) {
if (t === 0 && t === 1)
return;
const s = [
this.$refs.contentBoxRef.offsetWidth * this.lastTransformData.scale,
this.$refs.contentBoxRef.offsetHeight * this.lastTransformData.scale
];
let a = this.scaleTranslateProportion;
if (e) {
const n = [
(this.$refs.containerRef.offsetWidth / 2 - this.lastTransformData.x) / this.lastTransformData.scale,
(this.$refs.containerRef.offsetHeight / 2 - this.lastTransformData.y) / this.lastTransformData.scale
];
a = [
n[0] / this.$refs.containerRef.offsetWidth,
n[1] / this.$refs.containerRef.offsetHeight
];
}
this.lastTransformData.x += s[0] * (1 - t) * a[0] || 0, this.lastTransformData.y += s[1] * (1 - t) * a[1] || 0, this.lastTransformData.scale *= t, this.setMatrix(this.$refs.contentBoxRef);
},
ensureScaleInRange(t) {
return t * 100 > this.maxScale ? t = this.maxScale / 100 : t * 100 < this.minScale && (t = this.minScale / 100), t;
}
}
}, b = {};
var z = /* @__PURE__ */ y(
X,
M,
R,
!1,
O,
"ef31f3e2",
null,
null
);
function O(t) {
for (let e in b)
this[e] = b[e];
}
const h = /* @__PURE__ */ function() {
return z.exports;
}();
h.install = function(t) {
t.component(h.name, h);
};
var m = function(e) {
m.installed || (m.installed = !0, e.component(h.name, h));
};
typeof window < "u" && window.Vue && m(window.Vue);
export {
h as GrabbingBox,
m as default,
Y as version
};