UNPKG

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
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 };