quasar-ui-danx 0.0.10 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. package/package.json +8 -2
  2. package/src/components/ActionTable/ActionTable.vue +143 -0
  3. package/src/components/ActionTable/BatchActionMenu.vue +60 -0
  4. package/src/components/ActionTable/EmptyTableState.vue +33 -0
  5. package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +36 -0
  6. package/src/components/ActionTable/Filters/FilterGroupItem.vue +28 -0
  7. package/src/components/ActionTable/Filters/FilterGroupList.vue +76 -0
  8. package/src/components/ActionTable/Filters/FilterListToggle.vue +50 -0
  9. package/src/components/ActionTable/Filters/FilterableField.vue +143 -0
  10. package/src/components/ActionTable/Filters/index.ts +5 -0
  11. package/src/components/ActionTable/Form/Fields/BooleanField.vue +37 -0
  12. package/src/components/ActionTable/Form/Fields/ConfirmPasswordField.vue +46 -0
  13. package/src/components/ActionTable/Form/Fields/DateField.vue +59 -0
  14. package/src/components/ActionTable/Form/Fields/DateRangeField.vue +110 -0
  15. package/src/components/ActionTable/Form/Fields/DateTimeField.vue +50 -0
  16. package/src/components/ActionTable/Form/Fields/DateTimePicker.vue +59 -0
  17. package/src/components/ActionTable/Form/Fields/EditableDiv.vue +39 -0
  18. package/src/components/ActionTable/Form/Fields/FieldLabel.vue +32 -0
  19. package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +78 -0
  20. package/src/components/ActionTable/Form/Fields/InlineDateTimeField.vue +44 -0
  21. package/src/components/ActionTable/Form/Fields/IntegerField.vue +26 -0
  22. package/src/components/ActionTable/Form/Fields/LabelValueBlock.vue +22 -0
  23. package/src/components/ActionTable/Form/Fields/LabeledInput.vue +63 -0
  24. package/src/components/ActionTable/Form/Fields/MultiFileField.vue +91 -0
  25. package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +57 -0
  26. package/src/components/ActionTable/Form/Fields/NewPasswordField.vue +39 -0
  27. package/src/components/ActionTable/Form/Fields/NumberField.vue +94 -0
  28. package/src/components/ActionTable/Form/Fields/NumberRangeField.vue +140 -0
  29. package/src/components/ActionTable/Form/Fields/SelectDrawer.vue +136 -0
  30. package/src/components/ActionTable/Form/Fields/SelectField.vue +318 -0
  31. package/src/components/ActionTable/Form/Fields/SelectWithChildrenField.vue +81 -0
  32. package/src/components/ActionTable/Form/Fields/SingleFileField.vue +78 -0
  33. package/src/components/ActionTable/Form/Fields/TextField.vue +82 -0
  34. package/src/components/ActionTable/Form/Fields/WysiwygField.vue +46 -0
  35. package/src/components/ActionTable/Form/Fields/index.ts +23 -0
  36. package/src/components/ActionTable/Form/RenderedForm.vue +76 -0
  37. package/src/components/ActionTable/Form/index.ts +2 -0
  38. package/src/components/ActionTable/RenderComponentColumn.vue +22 -0
  39. package/src/components/ActionTable/TableSummaryRow.vue +95 -0
  40. package/src/components/ActionTable/index.ts +10 -0
  41. package/src/components/ActionTable/listActions.ts +362 -0
  42. package/src/components/ActionTable/listHelpers.ts +74 -0
  43. package/src/components/ActionTable/tableColumns.ts +72 -0
  44. package/src/components/DragAndDrop/HandleDraggable.vue +29 -29
  45. package/src/components/DragAndDrop/ListItemDraggable.vue +10 -10
  46. package/src/components/DragAndDrop/index.ts +0 -1
  47. package/src/components/DragAndDrop/listDragAndDrop.ts +1 -1
  48. package/src/components/Utility/CollapsableSidebar.vue +119 -0
  49. package/src/components/Utility/ContentDrawer.vue +70 -0
  50. package/src/components/Utility/Dialogs/ConfirmDialog.vue +132 -0
  51. package/src/components/Utility/Dialogs/FullScreenDialog.vue +46 -0
  52. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +105 -0
  53. package/src/components/Utility/Dialogs/InfoDialog.vue +92 -0
  54. package/src/components/Utility/Dialogs/InputDialog.vue +35 -0
  55. package/src/components/Utility/ImagePreview.vue +192 -0
  56. package/src/components/Utility/Popover/PopoverMenu.vue +64 -0
  57. package/src/components/Utility/Transitions/ListTransition.vue +50 -0
  58. package/src/components/Utility/Transitions/SlideTransition.vue +63 -0
  59. package/src/components/Utility/Transitions/StaggeredListTransition.vue +97 -0
  60. package/src/components/Utility/index.ts +11 -0
  61. package/src/components/index.ts +3 -0
  62. package/src/helpers/FileUpload.ts +295 -0
  63. package/src/helpers/FlashMessages.ts +79 -0
  64. package/src/helpers/array.ts +37 -0
  65. package/src/helpers/compatibility.ts +64 -0
  66. package/src/helpers/date.ts +5 -0
  67. package/src/helpers/download.ts +200 -0
  68. package/src/helpers/downloadPdf.ts +92 -0
  69. package/src/helpers/files.ts +52 -0
  70. package/src/helpers/formats.ts +183 -0
  71. package/src/helpers/http.ts +62 -0
  72. package/src/helpers/index.ts +12 -1
  73. package/src/helpers/multiFileUpload.ts +68 -0
  74. package/src/helpers/singleFileUpload.ts +54 -0
  75. package/src/helpers/storage.ts +8 -0
  76. package/src/index.esm.js +3 -4
  77. package/src/svg/FilterIcon.svg +7 -0
  78. package/src/svg/ImageIcon.svg +30 -0
  79. package/src/svg/PdfIcon.svg +21 -0
  80. package/src/svg/PercentIcon.svg +13 -0
  81. package/src/svg/TrashIcon.svg +15 -0
  82. package/src/svg/XIcon.svg +18 -0
  83. package/src/svg/index.ts +8 -0
  84. package/src/vendor/tinymce-config.ts +1 -0
  85. package/src/vue-plugin.js +7 -4
  86. package/tsconfig.json +14 -13
  87. package/src/components/DragAndDrop/Icons/index.ts +0 -2
  88. /package/src/{components/DragAndDrop/Icons → svg}/DragHandleDotsIcon.svg +0 -0
  89. /package/src/{components/DragAndDrop/Icons → svg}/DragHandleIcon.svg +0 -0
@@ -1,53 +1,53 @@
1
1
  <template>
2
2
  <div
3
- :class="{
3
+ :class="{
4
4
  'cursor-ew-resize': direction === 'horizontal',
5
5
  'cursor-ns-resize': direction === 'vertical',
6
6
  }"
7
- class="flex justify-center items-center w-full h-full"
8
- draggable="true"
9
- @dragstart="dragAndDrop.dragStart"
10
- @dragend="dragAndDrop.dragEnd"
7
+ class="flex justify-center items-center w-full h-full"
8
+ draggable="true"
9
+ @dragstart="dragAndDrop.dragStart"
10
+ @dragend="dragAndDrop.dragEnd"
11
11
  >
12
12
  <slot />
13
13
  </div>
14
14
  </template>
15
15
  <script setup>
16
- import { useDebounceFn } from "@vueuse/core";
17
- import { DragAndDrop } from "./dragAndDrop";
16
+ import { DragAndDrop } from '@ui/components';
17
+ import { useDebounceFn } from '@vueuse/core';
18
18
 
19
- const emit = defineEmits(["start", "end", "resize"]);
19
+ const emit = defineEmits(['start', 'end', 'resize']);
20
20
  const props = defineProps({
21
21
  initialValue: {
22
22
  type: Number,
23
- default: null,
23
+ default: null
24
24
  },
25
25
  dropZone: {
26
26
  type: [Function, String],
27
- required: true,
27
+ required: true
28
28
  },
29
29
  direction: {
30
30
  type: String,
31
- default: "horizontal",
32
- validator: (value) => ["vertical", "horizontal"].includes(value),
33
- },
31
+ default: 'horizontal',
32
+ validator: (value) => ['vertical', 'horizontal'].includes(value)
33
+ }
34
34
  });
35
35
 
36
36
  const dragAndDrop = new DragAndDrop()
37
- .setDropZone(props.dropZone)
38
- .setOptions({
39
- showPlaceholder: true,
40
- direction: props.direction,
41
- hideDragImage: true,
42
- })
43
- .onDragging(useDebounceFn(() => {
44
- emit("resize", {
45
- distance: dragAndDrop.getDistance(),
46
- percent: dragAndDrop.getPercentChange(),
47
- startDropZoneSize: dragAndDrop.startSize,
48
- dropZoneSize: dragAndDrop.getDropZoneSize(),
49
- });
50
- }, 20, { maxWait: 30 }))
51
- .onStart(() => emit("start"))
52
- .onEnd(() => emit("end"));
37
+ .setDropZone(props.dropZone)
38
+ .setOptions({
39
+ showPlaceholder: true,
40
+ direction: props.direction,
41
+ hideDragImage: true
42
+ })
43
+ .onDragging(useDebounceFn(() => {
44
+ emit('resize', {
45
+ distance: dragAndDrop.getDistance(),
46
+ percent: dragAndDrop.getPercentChange(),
47
+ startDropZoneSize: dragAndDrop.startSize,
48
+ dropZoneSize: dragAndDrop.getDropZoneSize()
49
+ });
50
+ }, 20, { maxWait: 30 }))
51
+ .onStart(() => emit('start'))
52
+ .onEnd(() => emit('end'));
53
53
  </script>
@@ -16,25 +16,25 @@
16
16
  </div>
17
17
  </template>
18
18
  <script setup>
19
- import SvgImg from "../Utility/SvgImg";
20
- import { HandleDraggableDotsIcon as DragHandleIcon } from "./Icons";
21
- import { ListDragAndDrop } from "./listDragAndDrop";
19
+ import { ListDragAndDrop } from '@ui/components';
20
+ import { DragHandleDotsIcon as DragHandleIcon } from '@ui/svg';
21
+ import SvgImg from '../Utility/SvgImg';
22
22
 
23
- const emit = defineEmits(["position", "update:list-items"]);
23
+ const emit = defineEmits(['position', 'update:list-items']);
24
24
  const props = defineProps({
25
25
  dropZone: {
26
26
  type: [Function, String],
27
- required: true,
27
+ required: true
28
28
  },
29
29
  direction: {
30
30
  type: String,
31
- default: "vertical",
32
- validator: (value) => ["vertical", "horizontal"].includes(value),
31
+ default: 'vertical',
32
+ validator: (value) => ['vertical', 'horizontal'].includes(value)
33
33
  },
34
34
  showHandle: Boolean,
35
35
  listItems: {
36
36
  type: Array,
37
- default: null,
37
+ default: null
38
38
  }
39
39
  });
40
40
 
@@ -42,12 +42,12 @@ const dragAndDrop = new ListDragAndDrop()
42
42
  .setDropZone(props.dropZone)
43
43
  .setOptions({ showPlaceholder: true, direction: props.direction })
44
44
  .onPositionChange((newPosition, oldPosition) => {
45
- emit("position", newPosition);
45
+ emit('position', newPosition);
46
46
 
47
47
  if (props.listItems) {
48
48
  const items = [...props.listItems];
49
49
  items.splice(newPosition, 0, items.splice(oldPosition, 1)[0]);
50
- emit("update:list-items", items);
50
+ emit('update:list-items', items);
51
51
  }
52
52
  });
53
53
  </script>
@@ -2,4 +2,3 @@ export { default as HandleDraggable } from "./HandleDraggable.vue";
2
2
  export { default as ListItemDraggable } from "./ListItemDraggable.vue";
3
3
  export { DragAndDrop } from "./dragAndDrop";
4
4
  export { ListDragAndDrop } from "./listDragAndDrop";
5
- export * from "./Icons";
@@ -1,4 +1,4 @@
1
- import { DragAndDrop } from "./dragAndDrop";
1
+ import { DragAndDrop } from "@ui/components";
2
2
 
3
3
  /**
4
4
  * ListDragAndDrop supports dragging elements in a list to new positions in the same list.
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <div
3
+ class="collapsable-sidebar overflow-x-hidden overflow-y-scroll relative"
4
+ :class="{
5
+ 'is-collapsed': isCollapsed,
6
+ 'is-right-side': rightSide,
7
+ [displayClass]: true,
8
+ }"
9
+ :style="style"
10
+ >
11
+ <div class="flex-grow max-w-full">
12
+ <slot :is-collapsed="isCollapsed" />
13
+ </div>
14
+ <template v-if="!disabled && (!hideToggleOnCollapse || !isCollapsed)">
15
+ <div
16
+ v-if="!toggleAtTop"
17
+ class="flex w-full p-4"
18
+ :class="rightSide ? 'justify-start' : 'justify-end'"
19
+ >
20
+ <slot name="toggle">
21
+ <q-btn
22
+ class="btn-secondary"
23
+ @click="toggleCollapse"
24
+ >
25
+ <ToggleIcon
26
+ class="w-5 transition-all"
27
+ :class="{ 'rotate-180': rightSide ? !isCollapsed : isCollapsed }"
28
+ />
29
+ </q-btn>
30
+ </slot>
31
+ </div>
32
+ <div
33
+ v-else
34
+ class="absolute top-0 right-0 cursor-pointer p-2"
35
+ :class="toggleClass"
36
+ @click="toggleCollapse"
37
+ >
38
+ <ToggleIcon
39
+ class="w-5 transition-all"
40
+ :class="{ 'rotate-180': rightSide ? !isCollapsed : isCollapsed }"
41
+ />
42
+ </div>
43
+ </template>
44
+ </div>
45
+ </template>
46
+ <script setup>
47
+ import { ChevronLeftIcon as ToggleIcon } from '@heroicons/vue/outline';
48
+ import { computed, onMounted, ref, watch } from 'vue';
49
+
50
+ const emit = defineEmits(['collapse', 'update:collapse']);
51
+ const props = defineProps({
52
+ rightSide: Boolean,
53
+ displayClass: {
54
+ type: String,
55
+ default: 'flex flex-col'
56
+ },
57
+ maxWidth: {
58
+ type: String,
59
+ default: '13.5rem'
60
+ },
61
+ minWidth: {
62
+ type: String,
63
+ default: '5.5rem'
64
+ },
65
+ disabled: Boolean,
66
+ collapse: Boolean,
67
+ name: {
68
+ type: String,
69
+ default: 'sidebar'
70
+ },
71
+ toggleAtTop: Boolean,
72
+ toggleClass: {
73
+ type: String,
74
+ default: ''
75
+ },
76
+ hideToggleOnCollapse: Boolean
77
+ });
78
+
79
+ const isCollapsed = ref(props.collapse);
80
+
81
+ const stored = localStorage.getItem(props.name + '-is-collapsed');
82
+
83
+ if (stored !== null) {
84
+ isCollapsed.value = stored === '1';
85
+ }
86
+ function toggleCollapse() {
87
+ setCollapse(!isCollapsed.value);
88
+ emit('collapse', isCollapsed.value);
89
+ emit('update:collapse', isCollapsed.value);
90
+ }
91
+
92
+ function setCollapse(state) {
93
+ isCollapsed.value = state;
94
+ localStorage.setItem(props.name + '-is-collapsed', isCollapsed.value ? '1' : '');
95
+ }
96
+
97
+ onMounted(() => {
98
+ emit('collapse', isCollapsed.value);
99
+ emit('update:collapse', isCollapsed.value);
100
+ });
101
+ const style = computed(() => {
102
+ return {
103
+ width: isCollapsed.value ? props.minWidth : props.maxWidth
104
+ };
105
+ });
106
+
107
+ watch(() => props.collapse, () => {
108
+ setCollapse(props.collapse);
109
+ });
110
+ </script>
111
+
112
+ <style
113
+ scoped
114
+ lang="scss"
115
+ >
116
+ .collapsable-sidebar {
117
+ @apply overflow-y-auto scroll-smooth flex-shrink-0 border-r border-neutral-plus-5 transition-all;
118
+ }
119
+ </style>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <q-dialog
3
+ v-model="isShowing"
4
+ maximized
5
+ :position="position"
6
+ :seamless="seamless"
7
+ :class="{'hide-backdrop': !overlay}"
8
+ >
9
+ <div>
10
+ <div
11
+ v-if="title"
12
+ class="dialog-title"
13
+ @click.stop.prevent
14
+ >
15
+ {{ title }}
16
+ </div>
17
+ <div
18
+ class="dialog-content bg-white"
19
+ :class="{ [contentClass]: true }"
20
+ >
21
+ <slot />
22
+ </div>
23
+ </div>
24
+ </q-dialog>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { computed } from 'vue';
29
+
30
+ const emit = defineEmits(['update:show']);
31
+
32
+ const props = defineProps({
33
+ show: Boolean,
34
+ seamless: Boolean,
35
+ overlay: Boolean,
36
+ position: {
37
+ type: String,
38
+ default: 'bottom'
39
+ },
40
+ contentClass: {
41
+ type: String,
42
+ default: 'py-8 px-12'
43
+ },
44
+ title: {
45
+ type: String,
46
+ default: 'Edit'
47
+ }
48
+ });
49
+
50
+ const isShowing = computed({
51
+ get: () => props.show,
52
+ set: (value) => emit('update:show', value)
53
+ });
54
+ </script>
55
+
56
+ <style
57
+ lang="scss"
58
+ scoped
59
+ >
60
+ .dialog-title {
61
+ @apply bg-gray-very-light text-gray-default font-medium uppercase text-xs px-6 py-3 border-b border-neutral-plus-5 rounded-t-md;
62
+ font-family: "Roboto", sans-serif;
63
+ letter-spacing: 0.05em;
64
+ box-shadow: 0px -4px 12px rgba(0, 0, 0, 0.25);
65
+ }
66
+
67
+ .dialog-content {
68
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
69
+ }
70
+ </style>
@@ -0,0 +1,132 @@
1
+ <template>
2
+ <q-dialog
3
+ :full-height="fullHeight"
4
+ :full-width="fullWidth"
5
+ :model-value="!!modelValue"
6
+ :no-backdrop-dismiss="!backdropDismiss"
7
+ :maximized="maximized"
8
+ @update:model-value="onClose"
9
+ >
10
+ <q-card class="flex flex-col flex-nowrap">
11
+ <q-card-section
12
+ v-if="title || $slots.title"
13
+ class="pl-6 pr-10 border-b border-gray-medium"
14
+ >
15
+ <h3
16
+ class="font-normal flex items-center"
17
+ :class="titleClass"
18
+ >
19
+ <slot name="title">{{ title }}</slot>
20
+ </h3>
21
+ <div
22
+ v-if="subtitle"
23
+ class="mt-1 text-sm"
24
+ >{{ subtitle }}
25
+ </div>
26
+ </q-card-section>
27
+ <q-card-section v-if="$slots.toolbar">
28
+ <slot name="toolbar" />
29
+ </q-card-section>
30
+ <q-card-section
31
+ v-if="content || $slots.default"
32
+ class="px-6 bg-neutral-plus-7 flex-grow max-h-full overflow-y-auto"
33
+ :class="contentClass"
34
+ >
35
+ <slot>{{ content }}</slot>
36
+ </q-card-section>
37
+ <div class="flex px-6 py-4 border-t border-gray-medium">
38
+ <div class="flex-grow">
39
+ <q-btn
40
+ :label="cancelText"
41
+ class="action-btn btn-white-gray"
42
+ @click="onClose"
43
+ >
44
+ <slot name="cancel-text" />
45
+ </q-btn>
46
+ </div>
47
+ <slot name="actions" />
48
+ <div v-if="!hideConfirm">
49
+ <q-btn
50
+ :label="$slots['confirm-text'] ? '' : confirmText"
51
+ class="action-btn ml-4"
52
+ :class="confirmClass"
53
+ :loading="isSaving"
54
+ :disable="disabled"
55
+ data-testid="confirm-button"
56
+ @click="onConfirm"
57
+ >
58
+ <slot name="confirm-text" />
59
+ </q-btn>
60
+ </div>
61
+ </div>
62
+ <a
63
+ class="absolute top-0 right-0 p-4 text-black"
64
+ @click="onClose"
65
+ >
66
+ <CloseIcon class="w-5" />
67
+ </a>
68
+ </q-card>
69
+ </q-dialog>
70
+ </template>
71
+
72
+ <script setup>
73
+ import { XIcon as CloseIcon } from '@heroicons/vue/outline';
74
+
75
+ const emit = defineEmits(['update:model-value', 'confirm', 'close']);
76
+ const props = defineProps({
77
+ modelValue: { type: [String, Boolean, Object], default: true },
78
+ title: {
79
+ type: String,
80
+ default: ''
81
+ },
82
+ titleClass: {
83
+ type: String,
84
+ default: ''
85
+ },
86
+ subtitle: {
87
+ type: String,
88
+ default: ''
89
+ },
90
+ content: {
91
+ type: String,
92
+ default: ''
93
+ },
94
+ backdropDismiss: Boolean,
95
+ maximized: Boolean,
96
+ fullWidth: Boolean,
97
+ fullHeight: Boolean,
98
+ disabled: Boolean,
99
+ isSaving: Boolean,
100
+ closeOnConfirm: Boolean,
101
+ hideConfirm: Boolean,
102
+ confirmText: {
103
+ type: String,
104
+ default: 'Confirm'
105
+ },
106
+ cancelText: {
107
+ type: String,
108
+ default: 'Cancel'
109
+ },
110
+ confirmClass: {
111
+ type: String,
112
+ default: 'bg-blue-base text-white'
113
+ },
114
+ contentClass: {
115
+ type: String,
116
+ default: ''
117
+ }
118
+ });
119
+
120
+ function onConfirm() {
121
+ emit('confirm');
122
+
123
+ if (props.closeOnConfirm) {
124
+ emit('close');
125
+ }
126
+ }
127
+
128
+ function onClose() {
129
+ emit('update:model-value', false);
130
+ emit('close');
131
+ }
132
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <q-dialog
3
+ :model-value="modelValue"
4
+ maximized
5
+ transition-show="slide-up"
6
+ transition-hide="slide-down"
7
+ @update:model-value="onClose"
8
+ >
9
+ <div class="flex justify-center min-w-xs" :class="computedClass">
10
+ <div
11
+ v-if="closeable"
12
+ v-close-popup
13
+ class="p-4 m-4 absolute-top-right top right cursor-pointer"
14
+ >
15
+ <XIcon class="w-5 h-5" />
16
+ </div>
17
+ <slot />
18
+ </div>
19
+ </q-dialog>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { XIcon } from '@ui/svg';
24
+ import { computed } from 'vue';
25
+
26
+ const emit = defineEmits(['update:model-value', 'close']);
27
+ const props = defineProps({
28
+ modelValue: Boolean,
29
+ center: Boolean,
30
+ blue: Boolean,
31
+ closeable: Boolean
32
+ });
33
+
34
+ let computedClass = computed(() => {
35
+ return {
36
+ 'bg-blue-base text-white': props.blue,
37
+ 'bg-white text-gray-base': !props.blue,
38
+ 'items-center': props.center
39
+ };
40
+ });
41
+
42
+ function onClose() {
43
+ emit('update:model-value', false);
44
+ emit('close');
45
+ }
46
+ </script>
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <q-dialog
3
+ :model-value="true"
4
+ maximized
5
+ @update:model-value="$emit('close')"
6
+ @keyup.left="carousel.previous()"
7
+ @keyup.right="carousel.next()"
8
+ >
9
+ <div class="absolute top-0 left-0 w-full h-full">
10
+ <q-carousel
11
+ ref="carousel"
12
+ v-model="currentSlide"
13
+ height="100%"
14
+ swipeable
15
+ animated
16
+ :thumbnails="files.length > 1"
17
+ infinite
18
+ class="carousel"
19
+ >
20
+ <q-carousel-slide
21
+ v-for="file in files"
22
+ :key="'file-' + file.id"
23
+ :name="file.id"
24
+ :img-src="getThumbUrl(file)"
25
+ >
26
+ <div class="slide-image">
27
+ <template v-if="isVideo(file)">
28
+ <video
29
+ class="max-h-full w-full"
30
+ controls
31
+ >
32
+ <source
33
+ :src="file.url + '#t=0.1'"
34
+ :type="file.mime"
35
+ />
36
+ </video>
37
+ </template>
38
+ <img v-else :alt="file.filename" :src="file.url" />
39
+ </div>
40
+ </q-carousel-slide>
41
+ </q-carousel>
42
+ <CloseIcon
43
+ class="absolute top-4 right-4 cursor-pointer text-white w-8 h-8"
44
+ @click="$emit('close')"
45
+ />
46
+ </div>
47
+ </q-dialog>
48
+ </template>
49
+ <script setup>
50
+ import { XIcon as CloseIcon } from '@ui/svg';
51
+ import { ref } from 'vue';
52
+
53
+ defineEmits(['close']);
54
+ const props = defineProps({
55
+ files: {
56
+ type: Array,
57
+ default: () => []
58
+ },
59
+ defaultSlide: {
60
+ type: String,
61
+ default: ''
62
+ }
63
+ });
64
+
65
+ const carousel = ref(null);
66
+ const currentSlide = ref(props.defaultSlide);
67
+ function isVideo(file) {
68
+ return file.mime?.startsWith('video');
69
+ }
70
+ function getThumbUrl(file) {
71
+ if (file.thumb) {
72
+ return file.thumb.url;
73
+ } else if (isVideo(file)) {
74
+ // Base64 encode a PlayIcon for the placeholder image
75
+ return `data:image/svg+xml;base64,${btoa(
76
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>`
77
+ )}`;
78
+ } else {
79
+ return file.url;
80
+ }
81
+ }
82
+ </script>
83
+ <style scoped lang="scss">
84
+ .slide-image {
85
+ width: 100%;
86
+ height: 100%;
87
+ background: black;
88
+ display: flex;
89
+ justify-content: center;
90
+ align-items: center;
91
+
92
+ img {
93
+ max-height: 100%;
94
+ max-width: 100%;
95
+ object-fit: contain;
96
+ }
97
+ }
98
+
99
+ .carousel {
100
+ :deep(.q-carousel__navigation--bottom) {
101
+ position: relative;
102
+ bottom: 8em;
103
+ }
104
+ }
105
+ </style>