@bagelink/vue 1.6.43 → 1.6.49

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.
Files changed (179) hide show
  1. package/bin/experimentalGenTypedRoutes.ts +18 -19
  2. package/bin/utils.ts +4 -4
  3. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  4. package/dist/components/Alert.vue.d.ts.map +1 -1
  5. package/dist/components/BglVideo.vue.d.ts.map +1 -1
  6. package/dist/components/Card.vue.d.ts.map +1 -1
  7. package/dist/components/Carousel.vue.d.ts +2 -2
  8. package/dist/components/Carousel.vue.d.ts.map +1 -1
  9. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  10. package/dist/components/Flag.vue.d.ts.map +1 -1
  11. package/dist/components/IframeVue.vue.d.ts.map +1 -1
  12. package/dist/components/ListItem.vue.d.ts.map +1 -1
  13. package/dist/components/Loading.vue.d.ts.map +1 -1
  14. package/dist/components/Modal.vue.d.ts.map +1 -1
  15. package/dist/components/ModalForm.vue.d.ts.map +1 -1
  16. package/dist/components/NavBar.vue.d.ts +1 -1
  17. package/dist/components/Pill.vue.d.ts.map +1 -1
  18. package/dist/components/Swiper.vue.d.ts +12 -4
  19. package/dist/components/Swiper.vue.d.ts.map +1 -1
  20. package/dist/components/Zoomer.vue.d.ts.map +1 -1
  21. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
  22. package/dist/components/analytics/PieChart.vue.d.ts +2 -1
  23. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
  24. package/dist/components/analytics/index.d.ts +1 -1
  25. package/dist/components/analytics/index.d.ts.map +1 -1
  26. package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
  27. package/dist/components/form/BglMultiStepForm.vue.d.ts.map +1 -1
  28. package/dist/components/form/inputs/ColorInput.vue.d.ts.map +1 -1
  29. package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
  30. package/dist/components/form/inputs/PasswordInput.vue.d.ts.map +1 -1
  31. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RangeInput.vue.d.ts +11 -11
  33. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  34. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -1
  35. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  36. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  37. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
  38. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  39. package/dist/components/layout/AppSidebar.vue.d.ts +1 -0
  40. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  41. package/dist/components/layout/Layout.vue.d.ts.map +1 -1
  42. package/dist/components/layout/Tabs.vue.d.ts.map +1 -1
  43. package/dist/components/layout/index.d.ts +3 -3
  44. package/dist/components/layout/index.d.ts.map +1 -1
  45. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  46. package/dist/index.cjs +24 -15
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.mjs +1530 -1404
  50. package/dist/plugins/modalTypes.d.ts +1 -8
  51. package/dist/plugins/modalTypes.d.ts.map +1 -1
  52. package/dist/plugins/useModal.d.ts.map +1 -1
  53. package/dist/style.css +1 -1
  54. package/package.json +1 -2
  55. package/src/components/AccordionItem.vue +13 -13
  56. package/src/components/AddToCalendar.vue +1 -1
  57. package/src/components/AddressSearch.vue +9 -8
  58. package/src/components/Alert.vue +2 -1
  59. package/src/components/Badge.vue +5 -5
  60. package/src/components/BglVideo.vue +44 -45
  61. package/src/components/Btn.vue +15 -15
  62. package/src/components/Card.vue +10 -8
  63. package/src/components/Carousel.vue +159 -162
  64. package/src/components/DataPreview.vue +1 -1
  65. package/src/components/DragOver.vue +6 -6
  66. package/src/components/Dropdown.vue +39 -38
  67. package/src/components/Flag.vue +7 -6
  68. package/src/components/Icon/Icon.vue +22 -22
  69. package/src/components/IframeVue.vue +5 -5
  70. package/src/components/Image.vue +17 -17
  71. package/src/components/ImportData.vue +79 -79
  72. package/src/components/ListItem.vue +12 -11
  73. package/src/components/Loading.vue +10 -9
  74. package/src/components/MapEmbed/Index.vue +24 -24
  75. package/src/components/Modal.vue +11 -9
  76. package/src/components/ModalForm.vue +15 -11
  77. package/src/components/NavBar.vue +6 -6
  78. package/src/components/Pagination.vue +27 -27
  79. package/src/components/Pill.vue +11 -12
  80. package/src/components/Rating.vue +2 -2
  81. package/src/components/Slider.vue +75 -75
  82. package/src/components/Spreadsheet/Index.vue +34 -34
  83. package/src/components/Spreadsheet/SpreadsheetTable.vue +3 -3
  84. package/src/components/Swiper.vue +4 -4
  85. package/src/components/Zoomer.vue +282 -182
  86. package/src/components/analytics/BarChart.vue +6 -6
  87. package/src/components/analytics/KpiCard.vue +2 -2
  88. package/src/components/analytics/LineChart.vue +63 -61
  89. package/src/components/analytics/PieChart.vue +104 -90
  90. package/src/components/analytics/index.ts +2 -2
  91. package/src/components/calendar/CalendarPopover.vue +1 -1
  92. package/src/components/calendar/Index.vue +1 -1
  93. package/src/components/calendar/views/AgendaView.vue +3 -3
  94. package/src/components/calendar/views/DayView.vue +6 -6
  95. package/src/components/calendar/views/MonthView.vue +2 -2
  96. package/src/components/calendar/views/WeekView.vue +18 -18
  97. package/src/components/dataTable/DataTable.vue +4 -4
  98. package/src/components/dataTable/useSorting.ts +1 -1
  99. package/src/components/dataTable/useTableData.ts +15 -15
  100. package/src/components/dataTable/useTableSelection.ts +15 -15
  101. package/src/components/dataTable/useTableVirtualization.ts +1 -1
  102. package/src/components/draggable/useDraggable.ts +42 -42
  103. package/src/components/form/BagelForm.vue +15 -15
  104. package/src/components/form/BglFieldSet.vue +5 -3
  105. package/src/components/form/BglMultiStepForm.vue +20 -21
  106. package/src/components/form/inputs/CheckInput.vue +2 -2
  107. package/src/components/form/inputs/CodeEditor/format.ts +7 -7
  108. package/src/components/form/inputs/CodeEditor/useHighlight.ts +6 -6
  109. package/src/components/form/inputs/ColorInput.vue +5 -4
  110. package/src/components/form/inputs/DateInput.vue +8 -9
  111. package/src/components/form/inputs/DatePicker.vue +24 -24
  112. package/src/components/form/inputs/EmailInput.vue +24 -24
  113. package/src/components/form/inputs/NumberInput.vue +26 -26
  114. package/src/components/form/inputs/OTP.vue +7 -7
  115. package/src/components/form/inputs/PasswordInput.vue +3 -2
  116. package/src/components/form/inputs/RadioGroup.vue +28 -25
  117. package/src/components/form/inputs/RadioPillsInput.vue +12 -12
  118. package/src/components/form/inputs/RangeInput.vue +21 -21
  119. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +107 -92
  120. package/src/components/form/inputs/RichText/components/TableGridSelector.vue +64 -64
  121. package/src/components/form/inputs/RichText/components/gridBox.vue +10 -8
  122. package/src/components/form/inputs/RichText/composables/useCommands.ts +1 -1
  123. package/src/components/form/inputs/RichText/composables/useEditor.ts +12 -12
  124. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +1 -1
  125. package/src/components/form/inputs/RichText/index.vue +138 -138
  126. package/src/components/form/inputs/RichText/utils/commands.ts +84 -85
  127. package/src/components/form/inputs/RichText/utils/debug.ts +1 -1
  128. package/src/components/form/inputs/RichText/utils/formatting.ts +39 -39
  129. package/src/components/form/inputs/RichText/utils/media.ts +7 -7
  130. package/src/components/form/inputs/RichText/utils/selection.ts +28 -28
  131. package/src/components/form/inputs/RichText/utils/table.ts +19 -19
  132. package/src/components/form/inputs/SelectBtn.vue +1 -1
  133. package/src/components/form/inputs/SelectInput.vue +54 -54
  134. package/src/components/form/inputs/SignaturePad.vue +40 -40
  135. package/src/components/form/inputs/TableField.vue +1 -1
  136. package/src/components/form/inputs/TelInput.vue +54 -53
  137. package/src/components/form/inputs/TextInput.vue +19 -19
  138. package/src/components/form/inputs/ToggleInput.vue +2 -2
  139. package/src/components/form/inputs/Upload/useFileUpload.ts +6 -6
  140. package/src/components/form/useBagelFormState.ts +5 -5
  141. package/src/components/layout/AppContent.vue +6 -3
  142. package/src/components/layout/AppLayout.vue +2 -2
  143. package/src/components/layout/AppSidebar.vue +83 -16
  144. package/src/components/layout/Layout.vue +12 -10
  145. package/src/components/layout/SidebarMenu.vue +4 -4
  146. package/src/components/layout/TabbedLayout.vue +17 -17
  147. package/src/components/layout/Tabs.vue +4 -5
  148. package/src/components/layout/TabsNav.vue +14 -14
  149. package/src/components/layout/index.ts +3 -5
  150. package/src/components/lightbox/Lightbox.vue +276 -126
  151. package/src/components/lightbox/index.ts +8 -8
  152. package/src/composables/index.ts +8 -8
  153. package/src/composables/useAddToCalendar.ts +13 -13
  154. package/src/composables/useDevice.ts +2 -2
  155. package/src/composables/useFormField.ts +4 -4
  156. package/src/composables/usePolling.ts +8 -8
  157. package/src/composables/useSchemaField.ts +38 -38
  158. package/src/composables/useTheme.ts +9 -9
  159. package/src/composables/useValidateFieldValue.ts +2 -2
  160. package/src/directives/pattern.ts +25 -25
  161. package/src/directives/ripple.ts +4 -4
  162. package/src/directives/vResize.ts +6 -6
  163. package/src/index.ts +1 -0
  164. package/src/plugins/bagel.ts +4 -4
  165. package/src/plugins/modalTypes.ts +1 -8
  166. package/src/plugins/useModal.ts +43 -18
  167. package/src/styles/layout.css +1 -1
  168. package/src/types/index.ts +1 -1
  169. package/src/utils/BagelFormUtils.ts +7 -7
  170. package/src/utils/calendar/Helpers.ts +8 -8
  171. package/src/utils/calendar/dateUtils.ts +22 -22
  172. package/src/utils/calendar/time.ts +25 -25
  173. package/src/utils/calendar/week.ts +25 -25
  174. package/src/utils/elementUtils.ts +27 -27
  175. package/src/utils/sizeParsing.ts +2 -2
  176. package/src/utils/strings.ts +5 -5
  177. package/src/utils/tapDetector.ts +11 -11
  178. package/src/utils/useSearch.ts +29 -29
  179. package/vite.config.ts +0 -2
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import { useDebounceFn } from '@bagelink/vue'
3
- import { onMounted, onUnmounted, watch } from 'vue'
3
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
4
4
  import TapDetector from '../utils/tapDetector'
5
5
 
6
6
  // Props interface
@@ -34,278 +34,364 @@ const {
34
34
  } = defineProps<Props>()
35
35
 
36
36
  const emit = defineEmits(['update:zoom'])
37
- // Reactive state using $ref (vue-macros)
38
- const zoomElement = $ref<HTMLElement | undefined>()
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,
37
+ // Reactive state using standard Vue composition API
38
+ const zoomElement = ref<HTMLElement | undefined>()
39
+ const containerWidth = ref(1)
40
+ const containerHeight = ref(1)
41
+ const containerLeft = ref(0)
42
+ const containerTop = ref(0)
43
+ const translateX = ref(0)
44
+ const animTranslateX = ref(0)
45
+ const translateY = ref(0)
46
+ const animTranslateY = ref(0)
47
+ const _zoom = ref(zoom ?? 1)
48
+ const scale = computed({
49
+ get: () => zoom ?? _zoom.value,
50
50
  set: (val) => {
51
- _zoom = val
52
- emit('update:zoom', _zoom)
51
+ _zoom.value = val
52
+ emit('update:zoom', _zoom.value)
53
53
  },
54
54
  })
55
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 | undefined>()
66
- let tapDetector = $ref<TapDetector | undefined>()
67
-
68
- let translateXValue = $ref(0)
69
- let translateYValue = $ref(0)
70
-
71
- const wrapperStyle = $computed(() => {
72
- translateXValue = containerWidth * animTranslateX
73
- translateYValue = containerHeight * animTranslateY
56
+ const animScale = ref(1)
57
+ const lastFullWheelTime = ref(0)
58
+ const lastWheelTime = ref(0)
59
+ const lastWheelDirection = ref<'x' | 'y'>('y')
60
+ const isPointerDown = ref(false)
61
+ const pointerPosX = ref(-1)
62
+ const pointerPosY = ref(-1)
63
+ const twoFingerInitDist = ref(0)
64
+ const panLocked = ref(true)
65
+ const raf = ref<number | undefined>()
66
+ const tapDetector = ref<TapDetector | undefined>()
67
+
68
+ // Momentum/inertia tracking
69
+ const velocityX = ref(0)
70
+ const velocityY = ref(0)
71
+ const lastMoveTime = ref(0)
72
+ const momentumRaf = ref<number | undefined>()
73
+
74
+ const wrapperStyle = computed(() => {
75
+ // Translation is in normalized coordinates (0.5 = half container)
76
+ // Convert to pixels
77
+ const translateXValue = containerWidth.value * animTranslateX.value
78
+ const translateYValue = containerHeight.value * animTranslateY.value
74
79
  return {
75
80
  transform: [
76
81
  `translate(${translateXValue}px, ${translateYValue}px)`,
77
- `scale(${animScale})`,
82
+ `scale(${animScale.value})`,
78
83
  ].join(' '),
84
+ transformOrigin: '50% 50%',
79
85
  }
80
86
  })
81
87
 
82
- watch(() => scale, (newScale) => {
88
+ watch(() => scale.value, (newScale) => {
83
89
  const { x, y } = calcTranslateLimit()
84
- translateX = Math.max(-x, Math.min(x, translateX))
85
- translateY = Math.max(-y, Math.min(y, translateY))
86
- if (1 !== newScale) {
87
- panLocked = false
90
+ translateX.value = Math.max(-x, Math.min(x, translateX.value))
91
+ translateY.value = Math.max(-y, Math.min(y, translateY.value))
92
+ if (newScale !== 1) {
93
+ panLocked.value = false
88
94
  }
89
95
  })
90
96
 
91
97
  function reset() {
92
- scale = 1
93
- panLocked = true
94
- translateX = 0
95
- translateY = 0
98
+ scale.value = 1
99
+ panLocked.value = true
100
+ translateX.value = 0
101
+ translateY.value = 0
96
102
  }
97
103
 
98
104
  defineExpose({ reset })
99
105
 
100
106
  function tryToScale(scaleDelta: number) {
101
- if (disabled) {return}
102
- let newScale = scale * scaleDelta
107
+ if (disabled) { return }
108
+ let newScale = scale.value * scaleDelta
103
109
  if (zoomingElastic) {
104
- if (newScale < minScale! || newScale > maxScale!) {
110
+ if (newScale < minScale || newScale > maxScale) {
105
111
  let log = Math.log2(scaleDelta)
106
112
  log *= 0.2
107
113
  scaleDelta = 2 ** log
108
- newScale = scale * scaleDelta
114
+ newScale = scale.value * scaleDelta
109
115
  }
110
116
  } else {
111
- if (newScale < minScale!) {newScale = minScale!}
112
- else if (newScale > maxScale!) {newScale = maxScale!}
117
+ if (newScale < minScale) {
118
+ newScale = minScale
119
+ } else if (newScale > maxScale) {
120
+ newScale = maxScale
121
+ }
113
122
  }
114
- scaleDelta = newScale / scale
115
- scale = newScale
116
- if ('image-center' !== pivot) {
117
- const normMousePosX = (pointerPosX - containerLeft) / containerWidth
118
- const normMousePosY = (pointerPosY - containerTop) / containerHeight
119
- translateX = (0.5 + translateX - normMousePosX) * scaleDelta + normMousePosX - 0.5
120
- translateY = (0.5 + translateY - normMousePosY) * scaleDelta + normMousePosY - 0.5
123
+ scaleDelta = newScale / scale.value
124
+ scale.value = newScale
125
+ if (pivot !== 'image-center') {
126
+ const normMousePosX = (pointerPosX.value - containerLeft.value) / containerWidth.value
127
+ const normMousePosY = (pointerPosY.value - containerTop.value) / containerHeight.value
128
+ translateX.value = (0.5 + translateX.value - normMousePosX) * scaleDelta + normMousePosX - 0.5
129
+ translateY.value = (0.5 + translateY.value - normMousePosY) * scaleDelta + normMousePosY - 0.5
121
130
  }
122
131
  }
123
132
 
124
133
  function setPointerPosCenter() {
125
- pointerPosX = containerLeft + containerWidth / 2
126
- pointerPosY = containerTop + containerHeight / 2
134
+ pointerPosX.value = containerLeft.value + containerWidth.value / 2
135
+ pointerPosY.value = containerTop.value + containerHeight.value / 2
127
136
  }
128
137
 
129
138
  function onPointerMove(newMousePosX: number, newMousePosY: number) {
130
- if (isPointerDown) {
131
- const pixelDeltaX = newMousePosX - pointerPosX
132
- const pixelDeltaY = newMousePosY - pointerPosY
133
- if (!panLocked) {
139
+ if (isPointerDown.value) {
140
+ const currentTime = Date.now()
141
+ const pixelDeltaX = newMousePosX - pointerPosX.value
142
+ const pixelDeltaY = newMousePosY - pointerPosY.value
143
+
144
+ if (!panLocked.value) {
145
+ // Calculate velocity for momentum
146
+ const timeDelta = currentTime - lastMoveTime.value
147
+ if (timeDelta > 0) {
148
+ velocityX.value = pixelDeltaX / timeDelta
149
+ velocityY.value = pixelDeltaY / timeDelta
150
+ }
151
+
134
152
  const translateLimit = calcTranslateLimit()
135
153
  const maxTranslateX = translateLimit.x
136
154
  const maxTranslateY = translateLimit.y
137
- const newTranslateX = translateX + pixelDeltaX / containerWidth
138
- const newTranslateY = translateY + pixelDeltaY / containerHeight
139
- translateX = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
140
- translateY = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))
155
+ const newTranslateX = translateX.value + pixelDeltaX / containerWidth.value
156
+ const newTranslateY = translateY.value + pixelDeltaY / containerHeight.value
157
+
158
+ translateX.value = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
159
+ translateY.value = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))
160
+
161
+ lastMoveTime.value = currentTime
141
162
  }
142
163
  }
143
- pointerPosX = newMousePosX
144
- pointerPosY = newMousePosY
164
+ pointerPosX.value = newMousePosX
165
+ pointerPosY.value = newMousePosY
166
+ }
167
+
168
+ function applyMomentum() {
169
+ if (Math.abs(velocityX.value) < 0.01 && Math.abs(velocityY.value) < 0.01) {
170
+ velocityX.value = 0
171
+ velocityY.value = 0
172
+ if (momentumRaf.value) {
173
+ cancelAnimationFrame(momentumRaf.value)
174
+ momentumRaf.value = undefined
175
+ }
176
+ return
177
+ }
178
+
179
+ const translateLimit = calcTranslateLimit()
180
+ const newTranslateX = translateX.value + velocityX.value * 16 / containerWidth.value
181
+ const newTranslateY = translateY.value + velocityY.value * 16 / containerHeight.value
182
+
183
+ translateX.value = Math.max(-translateLimit.x, Math.min(translateLimit.x, newTranslateX))
184
+ translateY.value = Math.max(-translateLimit.y, Math.min(translateLimit.y, newTranslateY))
185
+
186
+ // Apply friction
187
+ velocityX.value *= 0.95
188
+ velocityY.value *= 0.95
189
+
190
+ momentumRaf.value = requestAnimationFrame(applyMomentum)
145
191
  }
146
192
 
147
193
  const onInteractionEnd = useDebounceFn(() => {
148
194
  limit()
149
- panLocked = 1 === scale
150
- }, 100)
195
+ panLocked.value = scale.value === 1
196
+
197
+ // Start momentum animation if there's significant velocity
198
+ if (!panLocked.value && (Math.abs(velocityX.value) > 0.1 || Math.abs(velocityY.value) > 0.1)) {
199
+ if (momentumRaf.value) cancelAnimationFrame(momentumRaf.value)
200
+ applyMomentum()
201
+ } else {
202
+ velocityX.value = 0
203
+ velocityY.value = 0
204
+ }
205
+ }, 50)
151
206
 
152
207
  function limit() {
153
- if (scale < minScale!) {
154
- scale = minScale!
155
- } else if (scale > maxScale!) {
156
- tryToScale(maxScale! / scale)
208
+ if (scale.value < minScale) {
209
+ scale.value = minScale
210
+ } else if (scale.value > maxScale) {
211
+ tryToScale(maxScale / scale.value)
157
212
  }
158
213
 
159
214
  if (limitTranslation) {
160
215
  const translateLimit = calcTranslateLimit()
161
- if (Math.abs(translateX) > translateLimit.x) {
162
- translateX *= translateLimit.x / Math.abs(translateX)
216
+ if (Math.abs(translateX.value) > translateLimit.x) {
217
+ translateX.value *= translateLimit.x / Math.abs(translateX.value)
163
218
  }
164
- if (Math.abs(translateY) > translateLimit.y) {
165
- translateY *= translateLimit.y / Math.abs(translateY)
219
+ if (Math.abs(translateY.value) > translateLimit.y) {
220
+ translateY.value *= translateLimit.y / Math.abs(translateY.value)
166
221
  }
167
222
  }
168
223
  }
169
224
 
170
225
  function calcTranslateLimit() {
171
- if ('y' === getMarginDirection()) {
172
- const imageToContainerRatio = containerWidth / aspectRatio / containerHeight
173
- let translateLimitY = (scale * imageToContainerRatio - 1) / 2
174
- if (0 > translateLimitY) {translateLimitY = 0}
175
- return {
176
- x: scale - 1, // Allow full movement to edges horizontally
177
- y: translateLimitY * 2 // Allow full movement to edges vertically
178
- }
179
- }
180
- const imageToContainerRatio = containerHeight * aspectRatio / containerWidth
181
- let translateLimitX = (scale * imageToContainerRatio - 1) / 2
182
- if (0 > translateLimitX) {translateLimitX = 0}
226
+ // When scale = 1, no panning needed (image fits in container)
227
+ // When scale > 1, we can pan to see parts of the image outside the viewport
228
+
229
+ // The image is scaled from center, so:
230
+ // - At scale 2, the image is 2x larger
231
+ // - The amount that extends beyond the container on EACH side is: (scale - 1) * containerSize / 2
232
+ // - In normalized coordinates (where 1.0 = full container), this is: (scale - 1) / 2
233
+
234
+ // Simple and correct approach:
235
+ // Max pan distance in normalized coords = (scale - 1) / 2
236
+ // This works for both X and Y directions regardless of aspect ratio
237
+
238
+ const basePanLimit = Math.max(0, (scale.value - 1) / 2)
239
+
183
240
  return {
184
- x: translateLimitX * 2, // Allow full movement to edges horizontally
185
- y: scale - 1 // Allow full movement to edges vertically
241
+ x: basePanLimit,
242
+ y: basePanLimit,
186
243
  }
187
244
  }
188
245
 
189
- function getMarginDirection() {
190
- const containerRatio = containerWidth / containerHeight
191
- return containerRatio > aspectRatio ? 'x' : 'y'
192
- }
193
-
194
246
  function onMouseWheel(ev: WheelEvent) {
195
- if (!mouseWheelToZoom) {return}
247
+ if (!mouseWheelToZoom) { return }
196
248
  ev.preventDefault()
197
249
  const currTime = Date.now()
198
- if (120 === Math.abs(ev.deltaY)) {
199
- if (50 < currTime - lastFullWheelTime) {
250
+ if (Math.abs(ev.deltaY) === 120) {
251
+ if (currTime - lastFullWheelTime.value > 50) {
200
252
  onMouseWheelDo(ev.deltaY)
201
- lastFullWheelTime = currTime
253
+ lastFullWheelTime.value = currTime
202
254
  }
203
255
  } else {
204
- if (50 < currTime - lastWheelTime) {
205
- lastWheelDirection = ev.deltaX > ev.deltaY ? 'x' : 'y'
206
- if ('y' === lastWheelDirection) {
256
+ if (currTime - lastWheelTime.value > 50) {
257
+ lastWheelDirection.value = ev.deltaX > ev.deltaY ? 'x' : 'y'
258
+ if (lastWheelDirection.value === 'y') {
207
259
  onMouseWheelDo(ev.deltaY)
208
260
  }
209
261
  }
210
- lastWheelTime = currTime
262
+ lastWheelTime.value = currTime
211
263
  }
212
264
  }
213
265
 
214
266
  function onMouseWheelDo(wheelDelta: number) {
215
- const scaleDelta = 1.25 ** (wheelDelta / 120)
267
+ // More responsive zoom speed
268
+ const scaleDelta = 1.15 ** (wheelDelta / 120)
216
269
  tryToScale(scaleDelta)
217
270
  onInteractionEnd()
218
271
  }
219
272
 
220
273
  function onMouseDown(ev: MouseEvent) {
221
274
  refreshContainerPos()
222
- isPointerDown = true
223
- pointerPosX = ev.clientX
224
- pointerPosY = ev.clientY
275
+ isPointerDown.value = true
276
+ pointerPosX.value = ev.clientX
277
+ pointerPosY.value = ev.clientY
278
+ lastMoveTime.value = Date.now()
279
+ velocityX.value = 0
280
+ velocityY.value = 0
281
+ if (momentumRaf.value) {
282
+ cancelAnimationFrame(momentumRaf.value)
283
+ momentumRaf.value = undefined
284
+ }
225
285
  document.addEventListener('mousemove', onMouseMove)
226
286
  document.addEventListener('mouseup', onMouseUp)
227
287
  }
228
288
 
229
289
  function onMouseUp() {
230
- isPointerDown = false
290
+ isPointerDown.value = false
231
291
  onInteractionEnd()
232
292
  document.removeEventListener('mouseup', onMouseUp)
233
293
  document.removeEventListener('mousemove', onMouseMove)
234
294
  }
235
295
 
236
296
  function onMouseMove(ev: MouseEvent) {
297
+ // Prevent swiper from handling mouse drag when panning zoomed image
298
+ if (!panLocked.value && scale.value > 1) {
299
+ ev.stopPropagation()
300
+ }
237
301
  onPointerMove(ev.clientX, ev.clientY)
238
302
  }
239
303
 
240
304
  function onTouchStart(ev: TouchEvent) {
241
- if (1 === ev.touches.length) {
305
+ if (ev.touches.length === 1) {
242
306
  refreshContainerPos()
243
- pointerPosX = ev.touches[0].clientX
244
- pointerPosY = ev.touches[0].clientY
245
- isPointerDown = true
246
- } else if (2 === ev.touches.length) {
247
- isPointerDown = true
248
- pointerPosX = (ev.touches[0].clientX + ev.touches[1].clientX) / 2
249
- pointerPosY = (ev.touches[0].clientY + ev.touches[1].clientY) / 2
307
+ pointerPosX.value = ev.touches[0].clientX
308
+ pointerPosY.value = ev.touches[0].clientY
309
+ lastMoveTime.value = Date.now()
310
+ velocityX.value = 0
311
+ velocityY.value = 0
312
+ if (momentumRaf.value) {
313
+ cancelAnimationFrame(momentumRaf.value)
314
+ momentumRaf.value = undefined
315
+ }
316
+ isPointerDown.value = true
317
+ } else if (ev.touches.length === 2) {
318
+ if (momentumRaf.value) {
319
+ cancelAnimationFrame(momentumRaf.value)
320
+ momentumRaf.value = undefined
321
+ }
322
+ velocityX.value = 0
323
+ velocityY.value = 0
324
+ isPointerDown.value = true
325
+ pointerPosX.value = (ev.touches[0].clientX + ev.touches[1].clientX) / 2
326
+ pointerPosY.value = (ev.touches[0].clientY + ev.touches[1].clientY) / 2
250
327
  const distX = ev.touches[0].clientX - ev.touches[1].clientX
251
328
  const distY = ev.touches[0].clientY - ev.touches[1].clientY
252
- twoFingerInitDist = Math.sqrt(distX * distX + distY * distY)
329
+ twoFingerInitDist.value = Math.sqrt(distX * distX + distY * distY)
253
330
  }
254
331
  document.addEventListener('touchend', onTouchEnd)
255
332
  }
256
333
 
257
334
  function onTouchEnd(ev: TouchEvent) {
258
- if (0 === ev.touches.length) {
259
- isPointerDown = false
260
- if (0.1 > Math.abs(scale - 1)) {scale = 1}
335
+ if (ev.touches.length === 0) {
336
+ isPointerDown.value = false
337
+ if (Math.abs(scale.value - 1) < 0.1) { scale.value = 1 }
261
338
  onInteractionEnd()
262
- } else if (1 === ev.touches.length) {
263
- pointerPosX = ev.touches[0].clientX
264
- pointerPosY = ev.touches[0].clientY
339
+ } else if (ev.touches.length === 1) {
340
+ pointerPosX.value = ev.touches[0].clientX
341
+ pointerPosY.value = ev.touches[0].clientY
265
342
  }
266
343
  document.removeEventListener('touchend', onTouchEnd)
267
344
  }
268
345
 
269
346
  function onTouchMove(ev: TouchEvent) {
270
- if (1 === ev.touches.length) {
347
+ if (ev.touches.length === 1) {
348
+ // Prevent swiper from handling touch when panning zoomed image
349
+ if (!panLocked.value && scale.value > 1) {
350
+ ev.stopPropagation()
351
+ }
271
352
  onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY)
272
- } else if (2
273
-
274
- === ev.touches.length) {
353
+ } else if (ev.touches.length === 2) {
354
+ // Always prevent default during pinch zoom
355
+ ev.stopPropagation()
275
356
  const distX = ev.touches[0].clientX - ev.touches[1].clientX
276
357
  const distY = ev.touches[0].clientY - ev.touches[1].clientY
277
358
  const newTwoFingerDist = Math.sqrt(distX * distX + distY * distY)
278
- tryToScale(newTwoFingerDist / twoFingerInitDist)
279
- twoFingerInitDist = newTwoFingerDist
359
+ tryToScale(newTwoFingerDist / twoFingerInitDist.value)
360
+ twoFingerInitDist.value = newTwoFingerDist
280
361
  }
281
362
  }
282
363
 
283
364
  function refreshContainerPos() {
284
- if (zoomElement) {
285
- const rect = zoomElement.getBoundingClientRect()
286
- containerLeft = rect.left
287
- containerTop = rect.top
365
+ if (zoomElement.value) {
366
+ const rect = zoomElement.value.getBoundingClientRect()
367
+ containerLeft.value = rect.left
368
+ containerTop.value = rect.top
288
369
  }
289
370
  }
290
371
 
291
372
  function loop() {
292
- animScale = gainOn(animScale, scale)
293
- animTranslateX = gainOn(animTranslateX, translateX)
294
- animTranslateY = gainOn(animTranslateY, translateY)
295
- raf = window.requestAnimationFrame(loop)
373
+ animScale.value = gainOn(animScale.value, scale.value)
374
+ animTranslateX.value = gainOn(animTranslateX.value, translateX.value)
375
+ animTranslateY.value = gainOn(animTranslateY.value, translateY.value)
376
+ raf.value = window.requestAnimationFrame(loop)
296
377
  }
297
378
 
298
379
  function gainOn(from: number, to: number) {
299
- const delta = (to - from) * 0.3
300
- return 1e-5 < Math.abs(delta) ? from + delta : to
380
+ // Smoother easing with better responsiveness
381
+ const delta = (to - from) * 0.4
382
+ return Math.abs(delta) > 1e-5 ? from + delta : to
301
383
  }
302
384
 
303
385
  // Lifecycle hooks
304
386
  onMounted(() => {
305
- tapDetector = new TapDetector()
306
- if (zoomElement) {tapDetector.attach(zoomElement)}
387
+ tapDetector.value = new TapDetector()
388
+ if (zoomElement.value) { tapDetector.value.attach(zoomElement.value) }
307
389
  if (doubleClickToZoom) {
308
- tapDetector.onDoubleTap(onDoubleTap as any)
390
+ tapDetector.value.onDoubleTap(onDoubleTap as any)
391
+ // Also add native dblclick event for mouse
392
+ if (zoomElement.value) {
393
+ zoomElement.value.addEventListener('dblclick', onDoubleTap as any)
394
+ }
309
395
  }
310
396
  window.addEventListener('resize', onWindowResize)
311
397
  onWindowResize()
@@ -314,28 +400,46 @@ onMounted(() => {
314
400
  })
315
401
 
316
402
  onUnmounted(() => {
317
- if (zoomElement) {tapDetector?.detach(zoomElement)}
403
+ if (zoomElement.value) {
404
+ tapDetector.value?.detach(zoomElement.value)
405
+ if (doubleClickToZoom) {
406
+ zoomElement.value.removeEventListener('dblclick', onDoubleTap as any)
407
+ }
408
+ }
318
409
  window.removeEventListener('resize', onWindowResize)
319
- if (raf) {window.cancelAnimationFrame(raf)}
410
+ if (raf.value) { window.cancelAnimationFrame(raf.value) }
411
+ if (momentumRaf.value) { window.cancelAnimationFrame(momentumRaf.value) }
320
412
  })
321
413
 
322
414
  function onDoubleTap(ev: MouseEvent) {
323
- if (1 === scale) {
324
- if (0 < ev.clientX) {
325
- pointerPosX = ev.clientX
326
- pointerPosY = ev.clientY
327
- }
328
- tryToScale(Math.min(3, maxScale!))
415
+ // Update pointer position if available
416
+ if (ev.clientX > 0) {
417
+ pointerPosX.value = ev.clientX
418
+ pointerPosY.value = ev.clientY
419
+ }
420
+
421
+ // Calculate zoom levels
422
+ const mediumZoom = Math.min(2.5, maxScale / 2 + 0.5)
423
+ const highZoom = maxScale
424
+
425
+ // Cycle through three states: 1x -> medium -> max -> 1x
426
+ if (scale.value === 1) {
427
+ // From no zoom -> medium zoom
428
+ tryToScale(mediumZoom)
429
+ } else if (scale.value < highZoom - 0.1) {
430
+ // From medium zoom -> max zoom
431
+ tryToScale(highZoom / scale.value)
329
432
  } else {
433
+ // From max zoom -> no zoom (reset)
330
434
  reset()
331
435
  }
332
436
  }
333
437
 
334
438
  function onWindowResize() {
335
- if (zoomElement) {
336
- const styles = window.getComputedStyle(zoomElement)
337
- containerWidth = Number.parseFloat(styles.width)
338
- containerHeight = Number.parseFloat(styles.height)
439
+ if (zoomElement.value) {
440
+ const styles = window.getComputedStyle(zoomElement.value)
441
+ containerWidth.value = Number.parseFloat(styles.width)
442
+ containerHeight.value = Number.parseFloat(styles.height)
339
443
  setPointerPosCenter()
340
444
  limit()
341
445
  }
@@ -344,16 +448,9 @@ function onWindowResize() {
344
448
 
345
449
  <template>
346
450
  <div
347
- ref="zoomElement"
348
- class="vue-zoomer"
349
- :style="{ backgroundColor }"
350
- @mousewheel="onMouseWheel"
351
- @DOMMouseScroll="onMouseWheel"
352
- @mousedown="onMouseDown"
353
- @mouseout="setPointerPosCenter"
354
- @focusout="setPointerPosCenter"
355
- @touchstart="onTouchStart"
356
- @touchmove="onTouchMove"
451
+ ref="zoomElement" class="vue-zoomer" :style="{ backgroundColor }" @mousewheel="onMouseWheel"
452
+ @DOMMouseScroll="onMouseWheel" @mousedown="onMouseDown" @mouseout="setPointerPosCenter"
453
+ @focusout="setPointerPosCenter" @touchstart="onTouchStart" @touchmove="onTouchMove"
357
454
  >
358
455
  <div class="zoomer" :style="wrapperStyle">
359
456
  <slot />
@@ -371,24 +468,27 @@ function onWindowResize() {
371
468
  z-index: 1000;
372
469
  height: 100px
373
470
  }
471
+
374
472
  .vue-zoomer {
375
- overflow: hidden;
473
+ overflow: hidden;
376
474
  }
475
+
377
476
  .zoomer {
378
- transform-origin: 50% 50%;
379
- width: 100%;
380
- height: 100%;
381
- max-width: 100vw;
382
- max-height: 100vh;
477
+ transform-origin: 50% 50%;
478
+ width: 100%;
479
+ height: 100%;
480
+ max-width: 100vw;
481
+ max-height: 100vh;
383
482
  }
384
- .zoomer > img {
385
- vertical-align: top;
386
- user-select: none;
387
- -moz-user-drag: none;
388
- -webkit-user-select: none;
389
- -moz-user-select: none;
390
- -ms-user-select: none;
391
- -webkit-user-drag: none;
392
- -moz-user-drag: none;
483
+
484
+ .zoomer>img {
485
+ vertical-align: top;
486
+ user-select: none;
487
+ -moz-user-drag: none;
488
+ -webkit-user-select: none;
489
+ -moz-user-select: none;
490
+ -ms-user-select: none;
491
+ -webkit-user-drag: none;
492
+ -moz-user-drag: none;
393
493
  }
394
494
  </style>