|
29 | 29 | > |
30 | 30 | <transition name="fade"> |
31 | 31 | <div |
32 | | - v-if="hoveredOption === option" |
| 32 | + v-if="hoveredOption === option.id" |
33 | 33 | class="qr-popup" |
34 | 34 | > |
35 | 35 | <img |
|
43 | 43 | </div> |
44 | 44 | </template> |
45 | 45 |
|
46 | | -<script> |
| 46 | +<script setup> |
| 47 | +import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue' |
47 | 48 | import QRCode from 'qrcode' |
48 | 49 | import config from 'config' |
49 | 50 |
|
50 | | -export default { |
51 | | - props: { |
52 | | - options: { |
53 | | - type: Array, |
54 | | - required: true |
55 | | - }, |
56 | | - modelValue: { |
57 | | - type: Object, |
58 | | - default: null |
59 | | - } |
60 | | - }, |
61 | | - emits: ['input', 'update:modelValue'], |
62 | | - data() { |
63 | | - return { |
64 | | - isOpen: false, |
65 | | - selectedOption: this.modelValue ? this.modelValue.label : null, |
66 | | - hoveredOption: null, |
67 | | - qrCodes: {} |
68 | | - } |
69 | | - }, |
70 | | - watch: { |
71 | | - modelValue(newVal) { |
72 | | - this.selectedOption = newVal ? newVal.label : null; |
73 | | - } |
74 | | - }, |
75 | | - mounted() { |
76 | | - document.addEventListener('click', this.outsideClick) |
77 | | - }, |
78 | | - beforeUnmount() { |
79 | | - document.removeEventListener('click', this.outsideClick) |
80 | | - }, |
81 | | - created() { |
82 | | - this.options.forEach(option => { |
83 | | - this.generateQRCode(option) |
84 | | - }) |
85 | | - }, |
86 | | - methods: { |
87 | | - selectOption(option) { |
88 | | - this.selectedOption = option.label |
89 | | - this.isOpen = false |
90 | | - this.$emit('update:modelValue', option) |
91 | | - this.$emit('input', option) |
92 | | - }, |
93 | | - outsideClick(event) { |
94 | | - const dropdown = this.$refs.dropdown |
95 | | - if (!dropdown.contains(event.target)) { |
96 | | - this.isOpen = false |
97 | | - } |
98 | | - }, |
99 | | - generateQRCode(option) { |
100 | | - if (!['ics', 'xml', 'myics', 'myxml'].includes(option.id)) { |
101 | | - return |
102 | | - } |
103 | | - const url = config.api.base + 'export-talk?export_type=' + option.id |
104 | | - QRCode.toDataURL(url, { scale: 1 }, (err, url) => { |
105 | | - if (!err) this.qrCodes[option.id] = url |
106 | | - }) |
107 | | - }, |
108 | | - setHoveredOption(option) { |
109 | | - if (['ics', 'xml', 'myics', 'myxml'].includes(option.id)) { |
110 | | - this.hoveredOption = option |
111 | | - } else { |
112 | | - this.hoveredOption = null |
113 | | - } |
114 | | - }, |
115 | | - clearHoveredOption(option) { |
116 | | - if (this.hoveredOption === option) { |
117 | | - this.hoveredOption = null |
118 | | - } |
119 | | - }, |
120 | | - } |
| 51 | +const props = defineProps({ |
| 52 | + options: { |
| 53 | + type: Array, |
| 54 | + required: true |
| 55 | + }, |
| 56 | + modelValue: { |
| 57 | + type: Object, |
| 58 | + default: null |
| 59 | + } |
| 60 | +}) |
| 61 | +
|
| 62 | +const emit = defineEmits(['input', 'update:modelValue']) |
| 63 | +
|
| 64 | +const dropdown = ref(null) |
| 65 | +const isOpen = ref(false) |
| 66 | +const selectedOption = ref(props.modelValue ? props.modelValue.label : null) |
| 67 | +const hoveredOption = ref(null) |
| 68 | +const qrCodes = reactive({}) |
| 69 | +
|
| 70 | +watch(() => props.modelValue, (newVal) => { |
| 71 | + selectedOption.value = newVal ? newVal.label : null |
| 72 | +}) |
| 73 | +
|
| 74 | +watch(() => props.options, (newOpts) => { |
| 75 | + // regenerate QR codes when options change |
| 76 | + for (const k in qrCodes) delete qrCodes[k] |
| 77 | + if (Array.isArray(newOpts)) { |
| 78 | + newOpts.forEach(option => generateQRCode(option)) |
| 79 | + } |
| 80 | +}, { immediate: true, deep: true }) |
| 81 | +
|
| 82 | +function selectOption(option) { |
| 83 | + selectedOption.value = option.label |
| 84 | + isOpen.value = false |
| 85 | + emit('update:modelValue', option) |
| 86 | + emit('input', option) |
121 | 87 | } |
| 88 | +
|
| 89 | +function outsideClick(event) { |
| 90 | + const dd = dropdown.value |
| 91 | + if (dd && !dd.contains(event.target)) { |
| 92 | + isOpen.value = false |
| 93 | + } |
| 94 | +} |
| 95 | +
|
| 96 | +async function generateQRCode(option) { |
| 97 | + if (!['ics', 'xml', 'myics', 'myxml'].includes(option.id)) return |
| 98 | + const url = config.api.base + 'export-talk?export_type=' + option.id |
| 99 | + // generate a larger image to avoid pixelation when scaled down |
| 100 | + const popupSize = 150 // CSS popup size in px |
| 101 | + const ratio = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1 |
| 102 | + const targetWidth = Math.ceil(popupSize * Math.max(1, ratio)) |
| 103 | + try { |
| 104 | + const dataUrl = await QRCode.toDataURL(url, { width: targetWidth, margin: 1, errorCorrectionLevel: 'H' }) |
| 105 | + qrCodes[option.id] = dataUrl |
| 106 | + } catch (err) { |
| 107 | + // Keep behavior silent on error (same as previous implementation). |
| 108 | + // Optional: console.error('QR generation failed', err) |
| 109 | + } |
| 110 | +} |
| 111 | +
|
| 112 | +function setHoveredOption(option) { |
| 113 | + if (['ics', 'xml', 'myics', 'myxml'].includes(option.id)) { |
| 114 | + hoveredOption.value = option.id |
| 115 | + } else { |
| 116 | + hoveredOption.value = null |
| 117 | + } |
| 118 | +} |
| 119 | +
|
| 120 | +function clearHoveredOption(option) { |
| 121 | + if (hoveredOption.value === option.id) hoveredOption.value = null |
| 122 | +} |
| 123 | +
|
| 124 | +onMounted(() => { |
| 125 | + document.addEventListener('click', outsideClick) |
| 126 | +}) |
| 127 | +
|
| 128 | +onBeforeUnmount(() => { |
| 129 | + document.removeEventListener('click', outsideClick) |
| 130 | +}) |
122 | 131 | </script> |
123 | 132 |
|
124 | 133 | <style> |
@@ -194,4 +203,14 @@ export default { |
194 | 203 | height: 20px; /* Adjust as needed */ |
195 | 204 | align-self: flex-end; |
196 | 205 | } |
| 206 | +
|
| 207 | +/* Ensure QR images keep sharp edges and are not blurry when displayed */ |
| 208 | +.dropdown-options img.default-image, |
| 209 | +.qr-popup img { |
| 210 | + display: block; |
| 211 | + object-fit: contain; |
| 212 | + image-rendering: -webkit-optimize-contrast; /* Safari */ |
| 213 | + image-rendering: crisp-edges; |
| 214 | + image-rendering: pixelated; /* fallback */ |
| 215 | +} |
197 | 216 | </style> |
0 commit comments