@bagelink/vue 1.6.43 → 1.6.47

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.
@@ -1,18 +1,36 @@
1
1
  <script setup lang="ts">
2
2
  import type { LightboxItem } from './lightbox.types'
3
3
 
4
- import { BglVideo, Btn, Icon, Zoomer, Image, normalizeURL, Carousel, downloadFile } from '@bagelink/vue'
5
- import { watch } from 'vue'
4
+ import { BglVideo, Btn, Icon, Zoomer, Image, normalizeURL, Swiper, downloadFile } from '@bagelink/vue'
5
+ import { watch as vueWatch } from 'vue'
6
6
 
7
7
  let isOpen = $ref(false)
8
8
  let group = $ref<LightboxItem[]>([])
9
9
  let currentIndex = $ref(0)
10
- let currentItem = $computed<LightboxItem>(() => group[currentIndex])
10
+ const currentItem = $computed<LightboxItem>(() => group[currentIndex])
11
+ let zoom = $ref(1)
12
+
13
+ const canSwipe = $computed(() => zoom === 1)
14
+
15
+ // Advanced options that update reactively
16
+ const swiperAdvancedOptions = $computed(() => ({
17
+ allowTouchMove: zoom === 1,
18
+ touchRatio: zoom === 1 ? 1 : 0,
19
+ simulateTouch: zoom === 1,
20
+ }))
11
21
 
12
22
  function open(item: LightboxItem, groupItems?: LightboxItem[]) {
13
23
  isOpen = true
14
24
  group = groupItems || [item]
15
- currentIndex = group.findIndex(({ src }) => item.src === src)
25
+ currentIndex = group.findIndex((groupItem) => {
26
+ const hasSrcMatch = item.src !== undefined && item.src !== null && item.src !== ''
27
+ && groupItem.src === item.src
28
+ const hasPathMatch = item.pathKey !== undefined && item.pathKey !== null && item.pathKey !== ''
29
+ && groupItem.pathKey === item.pathKey
30
+ return hasSrcMatch || hasPathMatch
31
+ })
32
+ if (currentIndex === -1) currentIndex = 0
33
+ zoom = 1
16
34
  document.addEventListener('keydown', handleKeydown)
17
35
  }
18
36
 
@@ -21,107 +39,85 @@ function close() {
21
39
  document.removeEventListener('keydown', handleKeydown)
22
40
  }
23
41
 
24
- function next() {
25
- if (group.length > 1) {
26
- currentIndex = (currentIndex + 1) % group.length
27
- currentItem = group[currentIndex]
28
- }
29
- }
30
-
31
- function prev() {
32
- if (group.length > 1) {
33
- currentIndex = (currentIndex - 1 + group.length) % group.length
34
- currentItem = group[currentIndex]
35
- }
36
- }
37
-
38
42
  function selectItem(index: number) {
39
43
  currentIndex = index
40
- currentItem = group[index]
44
+ zoom = 1
45
+ // The v-model will handle updating the swiper
41
46
  }
42
47
 
43
- watch(() => isOpen, (val) => {
44
- if (val) { document.body.style.overflow = 'hidden' }
45
- else { document.body.style.overflow = '' }
48
+ vueWatch(() => isOpen, (val) => {
49
+ if (val) {
50
+ document.body.style.overflow = 'hidden'
51
+ } else {
52
+ document.body.style.overflow = ''
53
+ }
54
+ })
55
+
56
+ vueWatch(() => currentIndex, () => {
57
+ // Reset zoom when changing slides
58
+ zoom = 1
46
59
  })
47
60
 
48
61
  function handleKeydown(event: KeyboardEvent) {
49
62
  if (event.key === 'Escape') {
50
63
  close()
51
- } else if (event.key === 'ArrowLeft') {
52
- prev()
53
- } else if (event.key === 'ArrowRight') {
54
- next()
55
64
  }
56
65
  }
57
66
 
58
- const zoom = $ref(1)
59
-
60
- function clickOutside() {
61
- if (zoom === 1) { close() }
62
- }
63
-
64
67
  defineExpose({ open, close })
65
68
  </script>
66
69
 
67
70
  <template>
68
71
  <transition name="fade">
69
- <div
70
- v-if="isOpen"
71
- class="bgl-lightbox-overlay fixed w-100 h-100 flex justify-content-center z-9999 inset mx-auto"
72
- @keydown.esc="close" @keydown.left="prev" @keydown.right="next" @click="clickOutside"
73
- >
74
- <div
75
- v-if="group && group.length > 1"
76
- class="navigation flex space-between px-3 w-100 absolute m_px-1 m_none z-9"
77
- >
78
- <Btn class="oval opacity-8" icon="arrow_back" color="black" @click="prev" />
79
-
80
- <Btn class="oval opacity-8" icon="arrow_forward" color="black" @click="next" />
81
- </div>
82
- <div class="bgl-lightbox relative txt-center" @click.stop>
83
- <div class="flex start fixed top-1 w-100 space-between px-1 z-9">
84
- <Btn flat class="color-white" icon="close" @click="close" />
85
- <div v-if="currentItem?.enableZoom && currentItem?.type === 'image'" class="center">
86
- <Btn flat class="color-white" icon="remove" :disabled="zoom === 1" @click="zoom--" />
87
- <Btn flat class="color-white" icon="zoom_in" :disabled="zoom === 1" @click="zoom = 1" />
88
- <Btn flat class="color-white" icon="add" :disabled="zoom === 3" @click="zoom++" />
89
- </div>
90
- <Btn
91
- v-if="currentItem?.openFile && currentItem?.src" class="color-white" round thin flat
92
- iconEnd="arrow_outward" value="Open File" :href="currentItem?.src" target="_blank"
93
- />
94
- <Btn
95
- v-if="currentItem?.download && currentItem?.src" class="color-white" round thin flat
96
- icon="download" value="Download File" @click="downloadFile(currentItem?.src)"
97
- />
98
- <div v-if="!currentItem?.openFile && !currentItem?.download" />
72
+ <div v-if="isOpen" class="bgl-lightbox-overlay" @keydown.esc="close">
73
+ <div class="flex start w-100 space-between p-025 z-9" @click="close">
74
+ <Btn flat class="color-white" icon="close" @click="close" />
75
+ <div v-if="currentItem?.enableZoom && currentItem?.type === 'image'" class="center">
76
+ <Btn flat class="color-white" icon="remove" :disabled="zoom === 1" @click="zoom--" />
77
+ <Btn flat class="color-white" icon="zoom_in" :disabled="zoom === 1" @click="zoom = 1" />
78
+ <Btn flat class="color-white" icon="add" :disabled="zoom === 3" @click="zoom++" />
99
79
  </div>
80
+ <Btn
81
+ v-if="currentItem?.openFile && currentItem?.src" class="color-white" round thin flat
82
+ iconEnd="arrow_outward" value="Open File" :href="currentItem?.src" target="_blank"
83
+ />
84
+ <Btn
85
+ v-if="currentItem?.download && currentItem?.src" class="color-white" round thin flat
86
+ icon="download" value="Download File" @click="downloadFile(currentItem?.src)"
87
+ />
88
+ <div v-if="!currentItem?.openFile && !currentItem?.download" />
89
+ </div>
100
90
 
101
- <Carousel
102
- v-model:index="currentIndex" :items="1" class="bgl-lightbox-item"
103
- :class="{ zoomed: zoom > 1 }" :freeDrag="zoom === 1"
104
- >
105
- <template v-for="item in group" :key="item.src">
91
+ <Swiper
92
+ v-if="group && group.length > 0" v-model:index="currentIndex" :items="group"
93
+ :initial-slide="currentIndex" class="bgl-lightbox-swiper" :class="{ zoomed: zoom > 1 }"
94
+ :navigation="group.length > 1" :grab-cursor="canSwipe" :keyboard="true" :loop="false" :speed="400"
95
+ :slides-per-view="1" :space-between="0" effect="slide" :advanced-options="swiperAdvancedOptions"
96
+ @click.stop
97
+ >
98
+ <template #default="{ item }">
99
+ <div class="bgl-lightbox-item">
106
100
  <Zoomer
107
101
  v-if="item.type === 'image'" v-model:zoom="zoom" :disabled="!item?.enableZoom"
108
- :mouse-wheel-to-zoom="false"
102
+ :mouse-wheel-to-zoom="false" :double-click-to-zoom="true" :max-scale="5" :min-scale="1"
103
+ :aspect-ratio="0" :limit-translation="true" @click.stop
109
104
  >
110
- <Image :draggable="false" :src="item?.src" alt="Preview" class="vw90 lightbox-image" />
105
+ <Image :draggable="false" :src="item?.src" alt="Preview" class="lightbox-image" />
111
106
  </Zoomer>
112
107
 
113
108
  <BglVideo
114
109
  v-else-if="item?.type === 'video' && item?.src" :src="item?.src" autoplay controls
115
- class="vw90"
110
+ class="lightbox-video"
116
111
  />
117
112
 
118
- <div v-else-if="item?.type === 'pdf' && item?.src" class="vw90">
113
+ <div v-else-if="item?.type === 'pdf' && item?.src" class="lightbox-pdf">
119
114
  <embed
120
115
  :src="normalizeURL(item?.src)" type="application/pdf" width="100%" height="1080"
121
- :title="item?.name" class="vw90"
116
+ :title="item?.name"
122
117
  >
123
118
  </div>
124
- <div v-else class="vw90">
119
+
120
+ <div v-else class="lightbox-file">
125
121
  <div class="file-info txt-white flex m_block align-items-start gap-025">
126
122
  <Icon class="m-0 m_none" icon="draft" :size="10" weight="12" />
127
123
  <Icon class="m-0 none m_block m_-mb-1" icon="draft" :size="4" weight="2" />
@@ -129,40 +125,39 @@ defineExpose({ open, close })
129
125
  <div class="txt-start">
130
126
  <p class="mx-0 light">
131
127
  File:
132
- <span class="semi word-break-all ">
133
- {{ item?.name }}
134
- </span>
128
+ <span class="semi word-break-all">{{ item?.name }}</span>
135
129
  </p>
136
- <p class="mx-0 ">
130
+ <p class="mx-0">
137
131
  Type:
138
- <span class="semi">
139
- {{ item?.type }}
140
- </span>
132
+ <span class="semi">{{ item?.type }}</span>
141
133
  </p>
142
134
  <Btn :href="item?.src" target="_blank" round thin class="mt-1" value="Open file" />
143
- <!-- <a :href="currentItem?.src" target="_blank">Open file</a> -->
144
135
  </div>
145
136
  </div>
146
137
  </div>
147
- </template>
148
- </Carousel>
149
- <div
150
- v-if="group && group.length > 1" class="flex justify-content-center mt-2 overflow
151
- p-1 fixed bottom start end gap-1 m_justify-content-start"
152
- >
153
- <template v-for="(item, index) in group" :key="index">
154
- <Image
155
- v-if="item.type === 'image'" class="thumbnail object-fit-cover hover
156
- opacity-5 rounded flex bg-popup justify-content-center align-items-center flex-shrink-0" :src="item.src" alt=""
157
- :class="{ active: currentIndex === index }" @click="selectItem(index)"
158
- />
159
- <Icon
160
- v-else class="thumbnail object-fit-cover hover
161
- opacity-5 ed flex bg-popup justify-content-center align-items-center flex-shrink-0" icon="description"
162
- :class="{ active: currentIndex === index }" @click="selectItem(index)"
163
- />
164
- </template>
165
- </div>
138
+ </div>
139
+ </template>
140
+
141
+ <template #prev-button="{ prev }">
142
+ <Btn icon="arrow_back" color="black" @click="prev" />
143
+ </template>
144
+
145
+ <template #next-button="{ next }">
146
+ <Btn icon="arrow_forward" color="black" @click="next" />
147
+ </template>
148
+ </Swiper>
149
+
150
+ <div v-if="group && group.length > 1" class="lightbox-thumbnails" @click.stop>
151
+ <template v-for="(item, index) in group" :key="index">
152
+ <Image
153
+ v-if="item.type === 'image'" class="thumbnail" :src="item.src" alt=""
154
+ :class="{ active: currentIndex === index }" @click.stop="selectItem(index)"
155
+ />
156
+ <Icon
157
+ v-else class="thumbnail thumbnail-icon" icon="description"
158
+ :class="{ active: currentIndex === index }" @click.stop="selectItem(index)"
159
+ />
160
+ </template>
166
161
  </div>
167
162
  </div>
168
163
  </transition>
@@ -189,70 +184,225 @@ defineExpose({ open, close })
189
184
  }
190
185
 
191
186
  .bgl-lightbox-overlay {
187
+ position: fixed;
188
+ width: 100vw;
189
+ height: 100vh;
190
+ display: grid;
191
+ grid-template-rows: 50px 1fr 100px;
192
+ z-index: 9999;
193
+ inset: 0;
192
194
  background: rgba(0, 0, 0, 0.8);
195
+ overflow: hidden;
196
+ }
197
+
198
+ /* Top controls row */
199
+ .bgl-lightbox-overlay>.flex:first-child {
200
+ grid-row: 1;
201
+ align-self: start;
202
+ }
203
+
204
+ /* Main content row - Swiper */
205
+ .bgl-lightbox-swiper {
206
+ grid-row: 2;
207
+ width: 100vw;
208
+ height: 100%;
209
+ overflow: hidden;
210
+ }
211
+
212
+ /* Bottom thumbnails row */
213
+ .lightbox-thumbnails {
214
+ grid-row: 3;
215
+ align-self: end;
193
216
  }
194
217
 
195
- .bgl-lightbox {
196
- max-height: 90%;
218
+ .bgl-lightbox-swiper .swiper {
219
+ width: 100%;
220
+ height: 100%;
221
+ }
222
+
223
+ .bgl-lightbox-swiper .swiper-wrapper {
224
+ align-items: center;
197
225
  }
198
226
 
199
227
  .bgl-lightbox-item {
200
- animation: 500ms ease bgl-lightbox-load;
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ width: 100%;
232
+ height: 100%;
233
+ animation: 200ms ease bgl-lightbox-load;
234
+ }
235
+
236
+ /* Navigation button styling */
237
+ .lightbox-nav-btn {
238
+ background: rgba(0, 0, 0, 0.6);
239
+ backdrop-filter: blur(8px);
240
+ border-radius: 50%;
241
+ width: 48px;
242
+ height: 48px;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ color: white;
247
+ cursor: pointer;
248
+ transition: all 0.2s ease;
249
+ }
250
+
251
+ .lightbox-nav-btn:hover {
252
+ background: rgba(0, 0, 0, 0.8);
253
+ transform: scale(1.05);
254
+ }
255
+
256
+ .lightbox-nav-btn:active {
257
+ transform: scale(0.95);
258
+ }
259
+
260
+ /* Adjust swiper navigation positioning */
261
+ .bgl-lightbox-swiper :deep(.swi-ctrl) {
262
+ padding: 0 1.5rem !important;
263
+ }
264
+
265
+ @media screen and (max-width: 900px) {
266
+ .bgl-lightbox-swiper :deep(.swi-ctrl) {
267
+ padding: 0 1rem !important;
268
+ }
269
+
270
+ .lightbox-nav-btn {
271
+ width: 40px;
272
+ height: 40px;
273
+ }
201
274
  }
202
275
 
203
276
  @keyframes bgl-lightbox-load {
204
277
  from {
205
- scale: 0.7;
278
+ opacity: 0;
279
+ transform: scale(0.95);
206
280
  }
207
281
 
208
282
  to {
209
- scale: 1;
210
-
283
+ opacity: 1;
284
+ transform: scale(1);
211
285
  }
212
286
  }
213
287
 
214
- .bgl-lightbox-item * {
215
- max-height: calc(80vh - 90px);
288
+ .lightbox-image,
289
+ .lightbox-video,
290
+ .lightbox-pdf,
291
+ .lightbox-file {
292
+ max-width: 90vw;
293
+ max-height: calc(100vh - 140px);
294
+ object-fit: contain;
216
295
  border-radius: 3px;
217
296
  margin: auto;
218
- animation: 200ms ease bgl-lightbox-load;
219
- transition: max-height 200ms ease;
220
297
  }
221
298
 
222
- .bgl-lightbox-item.zoomed * {
299
+ .lightbox-image {
300
+ width: auto;
301
+ height: auto;
302
+ }
303
+
304
+ /* When zoomed, make image full width */
305
+ .bgl-lightbox-swiper.zoomed .lightbox-image {
306
+ max-width: 100vw;
307
+ width: 100vw;
308
+ max-height: 100vh;
309
+ height: 100vh;
310
+ object-fit: contain;
311
+ }
312
+
313
+ .bgl-lightbox-swiper.zoomed .vue-zoomer {
314
+ width: 100vw;
315
+ height: 100vh;
316
+ }
317
+
318
+ .bgl-lightbox-swiper.zoomed .bgl-lightbox-item * {
223
319
  max-height: calc(100vh - 90px);
224
320
  height: calc(100vh - 90px);
225
321
  }
226
322
 
227
- .bgl-lightbox-item.zoomed {
323
+ .bgl-lightbox-swiper.zoomed {
228
324
  pointer-events: none;
229
325
  }
230
326
 
231
- .bgl-lightbox-item.zoomed .vue-zoomer {
327
+ .bgl-lightbox-swiper.zoomed .vue-zoomer {
232
328
  pointer-events: auto;
233
329
  }
234
330
 
235
- .navigation {
236
- top: 50%;
237
- transform: translateY(-50%);
331
+ .lightbox-thumbnails {
332
+ display: flex;
333
+ justify-content: center;
334
+ align-items: center;
335
+ gap: 0.75rem;
336
+ padding: 1rem;
337
+ overflow-x: auto;
338
+ overflow-y: hidden;
339
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.4) 0%, transparent 100%);
340
+ width: 100%;
341
+ }
342
+
343
+ .lightbox-thumbnails::-webkit-scrollbar {
344
+ height: 4px;
345
+ }
346
+
347
+ .lightbox-thumbnails::-webkit-scrollbar-track {
348
+ background: transparent;
349
+ }
350
+
351
+ .lightbox-thumbnails::-webkit-scrollbar-thumb {
352
+ background: rgba(255, 255, 255, 0.3);
353
+ border-radius: 2px;
238
354
  }
239
355
 
240
356
  .thumbnail {
241
- height: 50px;
242
- width: 50px;
357
+ height: 60px;
358
+ width: 60px;
359
+ min-width: 60px;
360
+ object-fit: cover;
361
+ border-radius: 6px;
362
+ cursor: pointer;
363
+ opacity: 0.6;
364
+ transition: all 0.2s ease;
365
+ flex-shrink: 0;
366
+ }
367
+
368
+ .thumbnail-icon {
369
+ display: flex;
370
+ align-items: center;
371
+ justify-content: center;
372
+ background: rgba(255, 255, 255, 0.1);
243
373
  }
244
374
 
245
375
  .thumbnail:hover {
246
376
  opacity: 1;
377
+ transform: scale(1.05);
247
378
  }
248
379
 
249
380
  .thumbnail:active {
250
- opacity: 0.8;
381
+ transform: scale(0.95);
251
382
  }
252
383
 
253
384
  .thumbnail.active {
254
385
  opacity: 1;
255
- outline: 2px solid white;
386
+ outline: 3px solid white;
387
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
388
+ }
389
+
390
+ @media screen and (max-width: 910px) {
391
+ .bgl-lightbox-overlay {
392
+ grid-template-rows: 50px 1fr 70px;
393
+ }
394
+
395
+ .lightbox-thumbnails {
396
+ justify-content: flex-start;
397
+ gap: 0.5rem;
398
+ padding: 0.75rem 1rem;
399
+ }
400
+
401
+ .thumbnail {
402
+ height: 50px;
403
+ width: 50px;
404
+ min-width: 50px;
405
+ }
256
406
  }
257
407
 
258
408
  .file-info {
@@ -46,7 +46,7 @@ export interface ModalFormComponentProps<T extends { [key: string]: any }> exten
46
46
  modalOptions: ModalFormOptions<T>
47
47
  }
48
48
 
49
- export interface ModalFormOptions<T> {
49
+ export interface ModalFormOptions<T> extends ModalOptions {
50
50
  'schema': MaybeRefOrGetter<BglFormSchemaT<T>>
51
51
  'modelValue'?: T
52
52
  'onUpdate:modelValue'?: (val: T) => void
@@ -58,11 +58,4 @@ export interface ModalFormOptions<T> {
58
58
  'deleteText'?: string
59
59
  'duplicateText'?: string
60
60
  'onError'?: (err: any) => void
61
- 'side'?: boolean
62
- 'title'?: string
63
- 'width'?: string
64
- 'dismissable'?: boolean
65
- 'visible'?: boolean
66
- 'actions'?: BtnOptions[]
67
- 'class'?: string
68
61
  }
@@ -80,6 +80,11 @@ export const ModalPlugin: Plugin = {
80
80
  // Make options reactive so updates propagate
81
81
  const reactiveOptions = reactive(options) as ModalOptions | ModalFormOptions<T>
82
82
 
83
+ // Ensure visible is set for modals
84
+ if (!('visible' in reactiveOptions)) {
85
+ reactiveOptions.visible = true
86
+ }
87
+
83
88
  const modalComponent = {
84
89
  modalOptions: reactiveOptions,
85
90
  modalType,
@@ -147,6 +152,11 @@ export const ModalPlugin: Plugin = {
147
152
  // Calculate z-index based on stack position
148
153
  const zIndex = BASE_Z_INDEX + index * 10
149
154
 
155
+ if (!modal || !modal.modalOptions) {
156
+ console.error('[BageLink Modal] Invalid modal in stack:', modal)
157
+ return null
158
+ }
159
+
150
160
  const props = {
151
161
  ...modal.modalOptions,
152
162
  'visible': true,
@@ -155,8 +165,14 @@ export const ModalPlugin: Plugin = {
155
165
  }
156
166
 
157
167
  switch (modal.modalType) {
158
- case 'modalForm':
168
+ case 'modalForm': {
169
+ const formOptions = modal.modalOptions as ModalFormOptions<any>
170
+ if (!formOptions.schema) {
171
+ console.error('[BageLink Modal] ModalForm requires a schema', modal.modalOptions)
172
+ return null
173
+ }
159
174
  return h(ModalForm, props as ComponentProps<typeof ModalForm>, modal.componentSlots)
175
+ }
160
176
  case 'confirmModal':
161
177
  return h(ModalConfirm, props as ModalConfirmOptions, {})
162
178
  default:
@@ -171,27 +187,36 @@ export const ModalPlugin: Plugin = {
171
187
 
172
188
  // Auto-mount modal container to document.body
173
189
  if (typeof document !== 'undefined' && typeof window !== 'undefined') {
174
- // Wait for app to be mounted before injecting
190
+ const mountModalContainer = () => {
191
+ const existingContainer = document.getElementById('bagelink-modal-root')
192
+ if (!existingContainer) {
193
+ // Create mount point
194
+ const container = document.createElement('div')
195
+ container.id = 'bagelink-modal-root'
196
+ document.body.appendChild(container)
197
+
198
+ // Create a separate app instance for modals to avoid context issues
199
+ const modalApp = createApp(ModalContainerComponent)
200
+
201
+ // Share the same context/plugins
202
+ modalApp._context = app._context
203
+
204
+ // Mount it
205
+ modalApp.mount(container)
206
+ }
207
+ }
208
+
209
+ // Try to mount immediately if DOM is ready
210
+ if (document.body) {
211
+ mountModalContainer()
212
+ }
213
+
214
+ // Also use mixin as fallback for cases where plugin is installed before DOM ready
175
215
  app.mixin({
176
216
  mounted() {
177
217
  // Only run once on the root component
178
218
  if (this.$root === this) {
179
- const existingContainer = document.getElementById('bagelink-modal-root')
180
- if (!existingContainer) {
181
- // Create mount point
182
- const container = document.createElement('div')
183
- container.id = 'bagelink-modal-root'
184
- document.body.appendChild(container)
185
-
186
- // Create a separate app instance for modals to avoid context issues
187
- const modalApp = createApp(ModalContainerComponent)
188
-
189
- // Share the same context/plugins
190
- modalApp._context = app._context
191
-
192
- // Mount it
193
- modalApp.mount(container)
194
- }
219
+ mountModalContainer()
195
220
  }
196
221
  }
197
222
  })