@bagelink/vue 0.0.753 → 0.0.755

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css CHANGED
@@ -3282,19 +3282,38 @@ tbody tr[data-v-680da9b9]:hover {
3282
3282
  background: var(--bgl-gray-light);
3283
3283
  }
3284
3284
 
3285
- .lightbox-image[data-v-0410e12d]{
3285
+ .vue-zoomer[data-v-5e5bef88] {
3286
+ overflow: hidden;
3287
+ }
3288
+ .zoomer[data-v-5e5bef88] {
3289
+ transform-origin: 50% 50%;
3290
+ width: 100%;
3291
+ height: 100%;
3292
+ }
3293
+ .zoomer > img[data-v-5e5bef88] {
3294
+ vertical-align: top;
3295
+ user-select: none;
3296
+ -moz-user-drag: none;
3297
+ -webkit-user-select: none;
3298
+ -moz-user-select: none;
3299
+ -ms-user-select: none;
3300
+ -webkit-user-drag: none;
3301
+ -moz-user-drag: none;
3302
+ }
3303
+
3304
+ .lightbox-image[data-v-d550287d]{
3286
3305
  object-fit: contain;
3287
3306
  }
3288
- .bgl-lightbox-overlay[data-v-0410e12d] {
3307
+ .bgl-lightbox-overlay[data-v-d550287d] {
3289
3308
  background: rgba(0, 0, 0, 0.8);
3290
3309
  }
3291
- .bgl-lightbox[data-v-0410e12d] {
3310
+ .bgl-lightbox[data-v-d550287d] {
3292
3311
  max-height: 90%;
3293
3312
  }
3294
- .bgl-lightbox-item[data-v-0410e12d]{
3295
- animation: 500ms ease bgl-lightbox-load-0410e12d;
3313
+ .bgl-lightbox-item[data-v-d550287d]{
3314
+ animation: 500ms ease bgl-lightbox-load-d550287d;
3296
3315
  }
3297
- @keyframes bgl-lightbox-load-0410e12d {
3316
+ @keyframes bgl-lightbox-load-d550287d {
3298
3317
  from {
3299
3318
  scale:0.7;
3300
3319
  }
@@ -3302,32 +3321,32 @@ to {
3302
3321
  scale:1;
3303
3322
  }
3304
3323
  }
3305
- .bgl-lightbox-item[data-v-0410e12d] * {
3324
+ .bgl-lightbox-item[data-v-d550287d] * {
3306
3325
  max-width: 90%;
3307
3326
  max-height: calc(80vh - 90px);
3308
3327
  border-radius: 3px;
3309
3328
  margin: auto;
3310
- animation: 200ms ease bgl-lightbox-load-0410e12d;
3329
+ animation: 200ms ease bgl-lightbox-load-d550287d;
3311
3330
  }
3312
- .navigation[data-v-0410e12d] {
3331
+ .navigation[data-v-d550287d] {
3313
3332
  top: 50%;
3314
3333
  transform: translateY(-50%);
3315
3334
  }
3316
- .navigation-btn[data-v-0410e12d] {
3335
+ .navigation-btn[data-v-d550287d] {
3317
3336
  width: var(--input-height);
3318
3337
  height: var(--input-height);
3319
3338
  }
3320
- .thumbnail[data-v-0410e12d] {
3339
+ .thumbnail[data-v-d550287d] {
3321
3340
  height: 50px;
3322
3341
  width: 50px;
3323
3342
  }
3324
- .thumbnail[data-v-0410e12d]:hover {
3343
+ .thumbnail[data-v-d550287d]:hover {
3325
3344
  opacity: 1;
3326
3345
  }
3327
- .thumbnail[data-v-0410e12d]:active {
3346
+ .thumbnail[data-v-d550287d]:active {
3328
3347
  opacity: 0.8;
3329
3348
  }
3330
- .thumbnail.active[data-v-0410e12d] {
3349
+ .thumbnail.active[data-v-d550287d] {
3331
3350
  opacity: 1;
3332
3351
  outline: 2px solid white;
3333
3352
  }
@@ -1,5 +1,5 @@
1
1
  import { Attributes, BglFormSchemaT } from '..';
2
- export declare function debounce(fn: () => void, delay?: number): void;
2
+ export declare function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void;
3
3
  export declare function keyToLabel(key?: string): string | undefined;
4
4
  export declare function copyText(text: string, cb?: (msg: string) => void): Promise<void>;
5
5
  export declare function initials(...strArr: string[]): string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG/D,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,KAAK,SAAM,QAGnD;AAED,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3D;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,iBAGtE;AAED,wBAAgB,QAAQ,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,UAG3C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,QAIrE;AAED,wBAAgB,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,UAKpE;AAED,wBAAgB,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,OAU7F;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,OAM9C;AAED,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,OAGzE;AAED,eAAO,MAAM,MAAM,eAAgB,GAAG,YAA0C,CAAA;AAEhF,OAAO,KAAK,cAAc,MAAM,kBAAkB,CAAA;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAEhC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAa3F;AAED,eAAO,MAAM,KAAK,QAAQ,MAAM,qBAA0D,CAAA;AAE1F,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE/D,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAYpH;AAED,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3D;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,iBAGtE;AAED,wBAAgB,QAAQ,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,UAG3C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,QAIrE;AAED,wBAAgB,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,UAKpE;AAED,wBAAgB,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,OAU7F;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,OAM9C;AAED,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,OAGzE;AAED,eAAO,MAAM,MAAM,eAAgB,GAAG,YAA0C,CAAA;AAEhF,OAAO,KAAK,cAAc,MAAM,kBAAkB,CAAA;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAEhC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExC,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAa3F;AAED,eAAO,MAAM,KAAK,QAAQ,MAAM,qBAA0D,CAAA;AAE1F,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvD"}
@@ -0,0 +1,30 @@
1
+ type TapCallback = (event: {
2
+ clientX: number;
3
+ clientY: number;
4
+ }) => void;
5
+ export default class TapDetector {
6
+ private singleTapCallbacks;
7
+ private doubleTapCallbacks;
8
+ private isTouchMode;
9
+ private lastTapTimestamp;
10
+ private tappedCount;
11
+ private touchMovedLength;
12
+ private lastPointerX;
13
+ private lastPointerY;
14
+ attach(dom: HTMLElement): void;
15
+ detach(dom: HTMLElement): void;
16
+ onSingleTap(callback: TapCallback): void;
17
+ onDoubleTap(callback: TapCallback): void;
18
+ private triggerCallbacks;
19
+ private onTouchStart;
20
+ private onTouchMove;
21
+ private onTouchEnd;
22
+ private onMouseDown;
23
+ private onMouseMove;
24
+ private onMouseUp;
25
+ private onPointerDown;
26
+ private onPointerUp;
27
+ private onPointerMove;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=tapDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tapDetector.d.ts","sourceRoot":"","sources":["../../src/utils/tapDetector.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GAAG,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAA;AAGxE,MAAM,CAAC,OAAO,OAAO,WAAW;IAC/B,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,YAAY,CAAI;IAExB,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAa9B,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAS9B,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAMxC,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAMxC,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,YAAY,CAKnB;IAED,OAAO,CAAC,WAAW,CAIlB;IAED,OAAO,CAAC,UAAU,CAEjB;IAED,OAAO,CAAC,WAAW,CAIlB;IAED,OAAO,CAAC,WAAW,CAIlB;IAED,OAAO,CAAC,SAAS,CAEhB;IAED,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,aAAa;CAOrB"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "0.0.753",
4
+ "version": "0.0.755",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -0,0 +1,377 @@
1
+ <script lang="ts" setup>
2
+ import { debounce } from '@bagelink/vue'
3
+ import { onMounted, onUnmounted, watch } from 'vue'
4
+ import TapDetector from '../utils/tapDetector'
5
+
6
+ // Props interface
7
+ interface Props {
8
+ minScale?: number
9
+ maxScale?: number
10
+ zoom?: number
11
+ resetTrigger?: number
12
+ aspectRatio?: number
13
+ backgroundColor?: string
14
+ pivot?: string // 'cursor' | 'image-center'
15
+ zoomingElastic?: boolean
16
+ limitTranslation?: boolean
17
+ doubleClickToZoom?: boolean
18
+ mouseWheelToZoom?: boolean
19
+ disabled?: boolean
20
+ }
21
+
22
+ const {
23
+ minScale = 1,
24
+ maxScale = 5,
25
+ zoom,
26
+ disabled = false,
27
+ aspectRatio = 1,
28
+ backgroundColor = 'transparent',
29
+ pivot = 'cursor',
30
+ zoomingElastic = true,
31
+ limitTranslation = true,
32
+ doubleClickToZoom = true,
33
+ mouseWheelToZoom = true,
34
+ } = defineProps<Props>()
35
+
36
+ const emit = defineEmits(['update:zoom'])
37
+ // Reactive state using $ref (vue-macros)
38
+ const zoomElement = $ref<HTMLElement | null>()
39
+ let containerWidth = $ref(1)
40
+ let containerHeight = $ref(1)
41
+ let containerLeft = $ref(0)
42
+ let containerTop = $ref(0)
43
+ let translateX = $ref(0)
44
+ let animTranslateX = $ref(0)
45
+ let translateY = $ref(0)
46
+ let animTranslateY = $ref(0)
47
+ let _zoom = $ref(zoom || 1)
48
+ let scale = $computed({
49
+ get: () => zoom === undefined ? _zoom : zoom,
50
+ set: (val) => {
51
+ _zoom = val
52
+ emit('update:zoom', _zoom)
53
+ },
54
+ })
55
+
56
+ let animScale = $ref(1)
57
+ let lastFullWheelTime = $ref(0)
58
+ let lastWheelTime = $ref(0)
59
+ let lastWheelDirection = $ref<'x' | 'y'>('y')
60
+ let isPointerDown = $ref(false)
61
+ let pointerPosX = $ref(-1)
62
+ let pointerPosY = $ref(-1)
63
+ let twoFingerInitDist = $ref(0)
64
+ let panLocked = $ref(true)
65
+ let raf = $ref<number | null>(null)
66
+ let tapDetector = $ref<TapDetector | null>(null)
67
+
68
+ const wrapperStyle = $computed(() => {
69
+ const translateXValue = containerWidth * animTranslateX
70
+ const translateYValue = containerHeight * animTranslateY
71
+ return {
72
+ transform: [
73
+ `translate(${translateXValue}px, ${translateYValue}px)`,
74
+ `scale(${animScale})`,
75
+ ].join(' '),
76
+ }
77
+ })
78
+
79
+ watch(() => scale, (newScale) => {
80
+ const { x, y } = calcTranslateLimit()
81
+ translateX = Math.max(-x, Math.min(x, translateX))
82
+ translateY = Math.max(-y, Math.min(y, translateY))
83
+ if (newScale !== 1) {
84
+ panLocked = false
85
+ }
86
+ })
87
+
88
+ function reset() {
89
+ scale = 1
90
+ panLocked = true
91
+ translateX = 0
92
+ translateY = 0
93
+ }
94
+
95
+ defineExpose({ reset })
96
+
97
+ function tryToScale(scaleDelta: number) {
98
+ if (disabled) return
99
+ let newScale = scale * scaleDelta
100
+ if (zoomingElastic) {
101
+ if (newScale < minScale! || newScale > maxScale!) {
102
+ let log = Math.log2(scaleDelta)
103
+ log *= 0.2
104
+ scaleDelta = 2 ** log
105
+ newScale = scale * scaleDelta
106
+ }
107
+ } else {
108
+ if (newScale < minScale!) newScale = minScale!
109
+ else if (newScale > maxScale!) newScale = maxScale!
110
+ }
111
+ scaleDelta = newScale / scale
112
+ scale = newScale
113
+ if (pivot !== 'image-center') {
114
+ const normMousePosX = (pointerPosX - containerLeft) / containerWidth
115
+ const normMousePosY = (pointerPosY - containerTop) / containerHeight
116
+ translateX = (0.5 + translateX - normMousePosX) * scaleDelta + normMousePosX - 0.5
117
+ translateY = (0.5 + translateY - normMousePosY) * scaleDelta + normMousePosY - 0.5
118
+ }
119
+ }
120
+
121
+ function setPointerPosCenter() {
122
+ pointerPosX = containerLeft + containerWidth / 2
123
+ pointerPosY = containerTop + containerHeight / 2
124
+ }
125
+
126
+ function onPointerMove(newMousePosX: number, newMousePosY: number) {
127
+ if (isPointerDown) {
128
+ const pixelDeltaX = newMousePosX - pointerPosX
129
+ const pixelDeltaY = newMousePosY - pointerPosY
130
+ if (!panLocked) {
131
+ const translateLimit = calcTranslateLimit()
132
+ const maxTranslateX = translateLimit.x
133
+ const maxTranslateY = translateLimit.y
134
+ const newTranslateX = translateX + pixelDeltaX / containerWidth
135
+ const newTranslateY = translateY + pixelDeltaY / containerHeight
136
+ translateX = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
137
+ translateY = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))
138
+ }
139
+ }
140
+ pointerPosX = newMousePosX
141
+ pointerPosY = newMousePosY
142
+ }
143
+
144
+ function onInteractionEnd() {
145
+ debounce(() => {
146
+ limit()
147
+ panLocked = scale === 1
148
+ }, 100)
149
+ }
150
+
151
+ function limit() {
152
+ if (scale < minScale!) {
153
+ scale = minScale!
154
+ } else if (scale > maxScale!) {
155
+ tryToScale(maxScale! / scale)
156
+ }
157
+
158
+ if (limitTranslation) {
159
+ const translateLimit = calcTranslateLimit()
160
+ if (Math.abs(translateX) > translateLimit.x) {
161
+ translateX *= translateLimit.x / Math.abs(translateX)
162
+ }
163
+ if (Math.abs(translateY) > translateLimit.y) {
164
+ translateY *= translateLimit.y / Math.abs(translateY)
165
+ }
166
+ }
167
+ }
168
+
169
+ function calcTranslateLimit() {
170
+ if (getMarginDirection() === 'y') {
171
+ const imageToContainerRatio = containerWidth / aspectRatio / containerHeight
172
+ let translateLimitY = (scale * imageToContainerRatio - 1) / 2
173
+ if (translateLimitY < 0) translateLimitY = 0
174
+ return { x: (scale - 1) / 2, y: translateLimitY }
175
+ } else {
176
+ const imageToContainerRatio = containerHeight * aspectRatio / containerWidth
177
+ let translateLimitX = (scale * imageToContainerRatio - 1) / 2
178
+ if (translateLimitX < 0) translateLimitX = 0
179
+ return { x: translateLimitX, y: (scale - 1) / 2 }
180
+ }
181
+ }
182
+
183
+ function getMarginDirection() {
184
+ const containerRatio = containerWidth / containerHeight
185
+ return containerRatio > aspectRatio ? 'x' : 'y'
186
+ }
187
+
188
+ function onMouseWheel(ev: WheelEvent) {
189
+ if (!mouseWheelToZoom) return
190
+ ev.preventDefault()
191
+ const currTime = Date.now()
192
+ if (Math.abs(ev.deltaY) === 120) {
193
+ if (currTime - lastFullWheelTime > 50) {
194
+ onMouseWheelDo(ev.deltaY)
195
+ lastFullWheelTime = currTime
196
+ }
197
+ } else {
198
+ if (currTime - lastWheelTime > 50) {
199
+ lastWheelDirection = ev.deltaX > ev.deltaY ? 'x' : 'y'
200
+ if (lastWheelDirection === 'y') {
201
+ onMouseWheelDo(ev.deltaY)
202
+ }
203
+ }
204
+ lastWheelTime = currTime
205
+ }
206
+ }
207
+
208
+ function onMouseWheelDo(wheelDelta: number) {
209
+ const scaleDelta = 1.25 ** (wheelDelta / 120)
210
+ tryToScale(scaleDelta)
211
+ onInteractionEnd()
212
+ }
213
+
214
+ function onMouseDown(ev: MouseEvent) {
215
+ refreshContainerPos()
216
+ isPointerDown = true
217
+ pointerPosX = ev.clientX
218
+ pointerPosY = ev.clientY
219
+ document.addEventListener('mousemove', onMouseMove)
220
+ document.addEventListener('mouseup', onMouseUp)
221
+ }
222
+
223
+ function onMouseUp() {
224
+ isPointerDown = false
225
+ onInteractionEnd()
226
+ document.removeEventListener('mouseup', onMouseUp)
227
+ document.removeEventListener('mousemove', onMouseMove)
228
+ }
229
+
230
+ function onMouseMove(ev: MouseEvent) {
231
+ onPointerMove(ev.clientX, ev.clientY)
232
+ }
233
+
234
+ function onTouchStart(ev: TouchEvent) {
235
+ if (ev.touches.length === 1) {
236
+ refreshContainerPos()
237
+ pointerPosX = ev.touches[0].clientX
238
+ pointerPosY = ev.touches[0].clientY
239
+ isPointerDown = true
240
+ } else if (ev.touches.length === 2) {
241
+ isPointerDown = true
242
+ pointerPosX = (ev.touches[0].clientX + ev.touches[1].clientX) / 2
243
+ pointerPosY = (ev.touches[0].clientY + ev.touches[1].clientY) / 2
244
+ const distX = ev.touches[0].clientX - ev.touches[1].clientX
245
+ const distY = ev.touches[0].clientY - ev.touches[1].clientY
246
+ twoFingerInitDist = Math.sqrt(distX * distX + distY * distY)
247
+ }
248
+ document.addEventListener('touchend', onTouchEnd)
249
+ }
250
+
251
+ function onTouchEnd(ev: TouchEvent) {
252
+ if (ev.touches.length === 0) {
253
+ isPointerDown = false
254
+ if (Math.abs(scale - 1) < 0.1) scale = 1
255
+ onInteractionEnd()
256
+ } else if (ev.touches.length === 1) {
257
+ pointerPosX = ev.touches[0].clientX
258
+ pointerPosY = ev.touches[0].clientY
259
+ }
260
+ document.removeEventListener('touchend', onTouchEnd)
261
+ }
262
+
263
+ function onTouchMove(ev: TouchEvent) {
264
+ if (ev.touches.length === 1) {
265
+ onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY)
266
+ } else if (ev.touches.length
267
+
268
+ === 2) {
269
+ const distX = ev.touches[0].clientX - ev.touches[1].clientX
270
+ const distY = ev.touches[0].clientY - ev.touches[1].clientY
271
+ const newTwoFingerDist = Math.sqrt(distX * distX + distY * distY)
272
+ tryToScale(newTwoFingerDist / twoFingerInitDist)
273
+ twoFingerInitDist = newTwoFingerDist
274
+ }
275
+ }
276
+
277
+ function refreshContainerPos() {
278
+ if (zoomElement) {
279
+ const rect = zoomElement.getBoundingClientRect()
280
+ containerLeft = rect.left
281
+ containerTop = rect.top
282
+ }
283
+ }
284
+
285
+ function loop() {
286
+ animScale = gainOn(animScale, scale)
287
+ animTranslateX = gainOn(animTranslateX, translateX)
288
+ animTranslateY = gainOn(animTranslateY, translateY)
289
+ raf = window.requestAnimationFrame(loop)
290
+ }
291
+
292
+ function gainOn(from: number, to: number) {
293
+ const delta = (to - from) * 0.3
294
+ return Math.abs(delta) > 1e-5 ? from + delta : to
295
+ }
296
+
297
+ // Lifecycle hooks
298
+ onMounted(() => {
299
+ tapDetector = new TapDetector()
300
+ if (zoomElement) tapDetector.attach(zoomElement)
301
+ if (doubleClickToZoom) {
302
+ tapDetector.onDoubleTap(onDoubleTap as any)
303
+ }
304
+ window.addEventListener('resize', onWindowResize)
305
+ onWindowResize()
306
+ refreshContainerPos()
307
+ loop()
308
+ })
309
+
310
+ onUnmounted(() => {
311
+ if (zoomElement) tapDetector?.detach(zoomElement)
312
+ window.removeEventListener('resize', onWindowResize)
313
+ if (raf) window.cancelAnimationFrame(raf)
314
+ })
315
+
316
+ function onDoubleTap(ev: MouseEvent) {
317
+ if (scale === 1) {
318
+ if (ev.clientX > 0) {
319
+ pointerPosX = ev.clientX
320
+ pointerPosY = ev.clientY
321
+ }
322
+ tryToScale(Math.min(3, maxScale!))
323
+ } else {
324
+ reset()
325
+ }
326
+ }
327
+
328
+ function onWindowResize() {
329
+ if (zoomElement) {
330
+ const styles = window.getComputedStyle(zoomElement)
331
+ containerWidth = Number.parseFloat(styles.width)
332
+ containerHeight = Number.parseFloat(styles.height)
333
+ setPointerPosCenter()
334
+ limit()
335
+ }
336
+ }
337
+ </script>
338
+
339
+ <template>
340
+ <div
341
+ ref="zoomElement"
342
+ class="vue-zoomer"
343
+ :style="{ backgroundColor }"
344
+ @mousewheel="onMouseWheel"
345
+ @DOMMouseScroll="onMouseWheel"
346
+ @mousedown="onMouseDown"
347
+ @mouseout="setPointerPosCenter"
348
+ @focusout="setPointerPosCenter"
349
+ @touchstart="onTouchStart"
350
+ @touchmove="onTouchMove"
351
+ >
352
+ <div class="zoomer" :style="wrapperStyle">
353
+ <slot />
354
+ </div>
355
+ </div>
356
+ </template>
357
+
358
+ <style scoped>
359
+ .vue-zoomer {
360
+ overflow: hidden;
361
+ }
362
+ .zoomer {
363
+ transform-origin: 50% 50%;
364
+ width: 100%;
365
+ height: 100%;
366
+ }
367
+ .zoomer > img {
368
+ vertical-align: top;
369
+ user-select: none;
370
+ -moz-user-drag: none;
371
+ -webkit-user-select: none;
372
+ -moz-user-select: none;
373
+ -ms-user-select: none;
374
+ -webkit-user-drag: none;
375
+ -moz-user-drag: none;
376
+ }
377
+ </style>
@@ -30,3 +30,4 @@ export { default as RouterWrapper } from './RouterWrapper.vue'
30
30
  export { default as TableSchema } from './TableSchema.vue'
31
31
  export { default as Title } from './Title.vue'
32
32
  export { default as TopBar } from './TopBar.vue'
33
+ export { default as Zoomer } from './Zoomer.vue'
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { BglVideo, Btn, Icon } from '@bagelink/vue'
2
+ import { BglVideo, Btn, Icon, Zoomer } from '@bagelink/vue'
3
3
  import { watch } from 'vue'
4
+
4
5
  import type { LightboxItem } from './lightbox.types'
5
6
 
6
7
  let isOpen = $ref(false)
@@ -62,6 +63,12 @@ function handleKeydown(event: KeyboardEvent) {
62
63
  }
63
64
  }
64
65
 
66
+ const zoom = $ref(1)
67
+
68
+ function clickOutside() {
69
+ if (zoom === 1) close()
70
+ }
71
+
65
72
  defineExpose({ open, close })
66
73
  </script>
67
74
 
@@ -73,7 +80,7 @@ defineExpose({ open, close })
73
80
  @keydown.esc="close"
74
81
  @keydown.left="prev"
75
82
  @keydown.right="next"
76
- @click="close"
83
+ @click="clickOutside"
77
84
  >
78
85
  <div v-if="group && group.length > 1" class="navigation flex space-between px-3 w-100 absolute">
79
86
  <Btn
@@ -81,6 +88,7 @@ defineExpose({ open, close })
81
88
  icon="arrow_back"
82
89
  @click="prev"
83
90
  />
91
+
84
92
  <Btn
85
93
  class="navigation-btn oval"
86
94
  icon="arrow_forward"
@@ -88,10 +96,26 @@ defineExpose({ open, close })
88
96
  />
89
97
  </div>
90
98
  <div class="bgl-lightbox relative txt-center" @click.stop>
91
- <Btn flat class="fixed top-1 start-1 color-white" icon="close" @click="close" />
99
+ <div class="flex start fixed top-1 w-100 space-between">
100
+ <Btn flat class="color-white" icon="close" @click="close" />
101
+ <div v-if="currentItem?.enableZoom && currentItem?.type === 'image'" class="center">
102
+ <Btn flat class="color-white" icon="remove" :disabled="zoom === 1" @click="zoom--" />
103
+ <Btn flat class="color-white" icon="zoom_in" :disabled="zoom === 1" @click="zoom = 1" />
104
+ <Btn flat class="color-white" icon="add" :disabled="zoom === 3" @click="zoom++" />
105
+ </div>
106
+ <Btn
107
+ v-if="currentItem?.openFile" class="color-white" round thin flat icon.end="arrow_outward"
108
+ value="Open File"
109
+ :href="currentItem?.src"
110
+ target="_blank"
111
+ />
112
+ <div v-else />
113
+ </div>
92
114
  <div class="bgl-lightbox-item">
93
115
  <template v-if="currentItem?.type === 'image'">
94
- <img :src="currentItem?.src" alt="Preview" class="vw90 lightbox-image">
116
+ <Zoomer v-model:zoom="zoom" :disabled="!currentItem?.enableZoom" :mouse-wheel-to-zoom="false">
117
+ <img :draggable="false" :src="currentItem?.src" alt="Preview" class="vw90 lightbox-image">
118
+ </Zoomer>
95
119
  </template>
96
120
  <template v-else-if="currentItem?.type === 'video'">
97
121
  <BglVideo
@@ -43,10 +43,10 @@ function openClickHandler(e: MouseEvent, el: HTMLElement, binding: DirectiveBind
43
43
  }
44
44
 
45
45
  function bindingToItem(binding: DirectiveBinding, el: HTMLElement): LightboxItem {
46
- let { group, src, type, name, thumbnail } = binding.value || {}
46
+ let { group, src, type, name, thumbnail, enableZoom, openFile } = binding.value || {}
47
47
  src = src || binding.value || el.getAttribute('src') || ''
48
48
  type = type || determineFileType(src)
49
- return { src, type, name, thumbnail, group }
49
+ return { src, type, name, thumbnail, group, enableZoom, openFile }
50
50
  }
51
51
 
52
52
  const youtubeRegex = /youtube\.com|youtu\.be/
@@ -4,4 +4,6 @@ export interface LightboxItem {
4
4
  name: string
5
5
  thumbnail?: string
6
6
  group?: string
7
+ enableZoom?: boolean
8
+ openFile?: boolean
7
9
  }
@@ -1,9 +1,17 @@
1
1
  import type { Attributes, BglFormSchemaT } from '@bagelink/vue'
2
2
 
3
- let timeout: any
4
- export function debounce(fn: () => void, delay = 500) {
5
- clearTimeout(timeout)
6
- timeout = setTimeout(fn, delay)
3
+ export function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void {
4
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
5
+
6
+ return function (...args: Parameters<T>) {
7
+ if (timeoutId) {
8
+ clearTimeout(timeoutId)
9
+ }
10
+
11
+ timeoutId = setTimeout(() => {
12
+ func(...args)
13
+ }, wait)
14
+ }
7
15
  }
8
16
 
9
17
  export function keyToLabel(key?: string): string | undefined {