Fix lightbox QR download: sync canvas.toDataURL
Async fetch/toBlob lost the user-gesture so Safari/Chrome opened the PNG instead of downloading. Use a button and sync data-URL save. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -516,8 +516,10 @@
|
|||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font: inherit;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||||
}
|
}
|
||||||
.lightbox-download:hover {
|
.lightbox-download:hover {
|
||||||
@@ -759,10 +761,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="lightbox-caption" id="lightbox-caption"></p>
|
<p class="lightbox-caption" id="lightbox-caption"></p>
|
||||||
<div class="lightbox-actions">
|
<div class="lightbox-actions">
|
||||||
<a class="lightbox-download" id="lightbox-download" href="#" download role="button">
|
<button type="button" class="lightbox-download" id="lightbox-download">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
Скачать QR
|
Скачать QR
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -827,6 +829,16 @@
|
|||||||
var downloadBtn = document.getElementById('lightbox-download');
|
var downloadBtn = document.getElementById('lightbox-download');
|
||||||
var downloadSrc = '';
|
var downloadSrc = '';
|
||||||
|
|
||||||
|
function triggerDataUrlDownload(dataUrl, filename) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = filename;
|
||||||
|
a.rel = 'noopener';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function triggerFileDownload(blob, filename) {
|
function triggerFileDownload(blob, filename) {
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
@@ -839,49 +851,36 @@
|
|||||||
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadQrImage(e) {
|
function downloadQrFromCanvas(filename) {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
var filename = downloadBtn.getAttribute('data-filename') || downloadBtn.getAttribute('download') || 'qr.png';
|
|
||||||
var src = downloadSrc || img.currentSrc || img.src;
|
|
||||||
|
|
||||||
function fromCanvas() {
|
|
||||||
try {
|
|
||||||
if (!img.complete || !img.naturalWidth) return false;
|
if (!img.complete || !img.naturalWidth) return false;
|
||||||
|
try {
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = img.naturalWidth;
|
canvas.width = img.naturalWidth;
|
||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
canvas.getContext('2d').drawImage(img, 0, 0);
|
||||||
canvas.toBlob(function (blob) {
|
// toDataURL sync — download stays in the user-gesture (Safari/Chrome)
|
||||||
if (blob) triggerFileDownload(blob, filename);
|
triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
|
||||||
}, 'image/png');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (src) {
|
function downloadQrImage(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
var filename = downloadBtn.getAttribute('data-filename') || 'qr.png';
|
||||||
|
if (downloadQrFromCanvas(filename)) return;
|
||||||
|
|
||||||
|
var src = downloadSrc || img.currentSrc || img.src;
|
||||||
|
if (!src) return;
|
||||||
fetch(src)
|
fetch(src)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error('fetch failed');
|
if (!res.ok) throw new Error('fetch failed');
|
||||||
return res.blob();
|
return res.blob();
|
||||||
})
|
})
|
||||||
.then(function (blob) { triggerFileDownload(blob, filename); })
|
.then(function (blob) { triggerFileDownload(blob, filename); })
|
||||||
.catch(function () {
|
.catch(function () { /* avoid navigating to the image URL */ });
|
||||||
if (!fromCanvas()) {
|
|
||||||
var a = document.createElement('a');
|
|
||||||
a.href = src;
|
|
||||||
a.download = filename;
|
|
||||||
a.rel = 'noopener';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fromCanvas();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLightbox(src, text, alt, filename) {
|
function openLightbox(src, text, alt, filename) {
|
||||||
@@ -890,8 +889,6 @@
|
|||||||
img.alt = alt || text || 'QR';
|
img.alt = alt || text || 'QR';
|
||||||
caption.textContent = text || '';
|
caption.textContent = text || '';
|
||||||
downloadSrc = src;
|
downloadSrc = src;
|
||||||
downloadBtn.href = '#';
|
|
||||||
downloadBtn.setAttribute('download', safeName);
|
|
||||||
downloadBtn.setAttribute('data-filename', safeName);
|
downloadBtn.setAttribute('data-filename', safeName);
|
||||||
downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
|
downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
|
||||||
box.classList.add('open');
|
box.classList.add('open');
|
||||||
@@ -938,6 +935,16 @@
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
function triggerDataUrlDownload(dataUrl, filename) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = filename;
|
||||||
|
a.rel = 'noopener';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function triggerFileDownload(blob, filename) {
|
function triggerFileDownload(blob, filename) {
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
@@ -953,16 +960,13 @@
|
|||||||
function pngFromCard(link, filename) {
|
function pngFromCard(link, filename) {
|
||||||
var card = link.closest('.user-card');
|
var card = link.closest('.user-card');
|
||||||
var qrImg = card && card.querySelector('img[data-lightbox-src], .qr-block img');
|
var qrImg = card && card.querySelector('img[data-lightbox-src], .qr-block img');
|
||||||
if (!qrImg) return false;
|
if (!qrImg || !qrImg.complete || !qrImg.naturalWidth) return false;
|
||||||
try {
|
try {
|
||||||
if (!qrImg.complete || !qrImg.naturalWidth) return false;
|
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = qrImg.naturalWidth;
|
canvas.width = qrImg.naturalWidth;
|
||||||
canvas.height = qrImg.naturalHeight;
|
canvas.height = qrImg.naturalHeight;
|
||||||
canvas.getContext('2d').drawImage(qrImg, 0, 0);
|
canvas.getContext('2d').drawImage(qrImg, 0, 0);
|
||||||
canvas.toBlob(function (blob) {
|
triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
|
||||||
if (blob) triggerFileDownload(blob, filename);
|
|
||||||
}, 'image/png');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -978,18 +982,13 @@
|
|||||||
var fallbackText = link.getAttribute('data-text');
|
var fallbackText = link.getAttribute('data-text');
|
||||||
var isPng = /\.png$/i.test(filename);
|
var isPng = /\.png$/i.test(filename);
|
||||||
|
|
||||||
function fallback() {
|
// Sync paths first — async fetch loses the user-gesture in Safari/Chrome
|
||||||
|
if (isPng && pngFromCard(link, filename)) return;
|
||||||
if (fallbackText !== null) {
|
if (fallbackText !== null) {
|
||||||
triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
|
triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPng) pngFromCard(link, filename);
|
if (!href) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!href) {
|
|
||||||
fallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(href)
|
fetch(href)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
@@ -997,7 +996,7 @@
|
|||||||
return res.blob();
|
return res.blob();
|
||||||
})
|
})
|
||||||
.then(function (blob) { triggerFileDownload(blob, filename); })
|
.then(function (blob) { triggerFileDownload(blob, filename); })
|
||||||
.catch(fallback);
|
.catch(function () { /* do not navigate to the file URL */ });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -333,8 +333,10 @@
|
|||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font: inherit;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.lightbox-download:hover {
|
.lightbox-download:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
@@ -463,10 +465,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="lightbox-caption" id="lightbox-caption"></p>
|
<p class="lightbox-caption" id="lightbox-caption"></p>
|
||||||
<div class="lightbox-actions">
|
<div class="lightbox-actions">
|
||||||
<a class="lightbox-download" id="lightbox-download" href="#" download role="button">
|
<button type="button" class="lightbox-download" id="lightbox-download">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
Скачать QR
|
Скачать QR
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,6 +506,16 @@
|
|||||||
var downloadBtn = document.getElementById('lightbox-download');
|
var downloadBtn = document.getElementById('lightbox-download');
|
||||||
var downloadSrc = '';
|
var downloadSrc = '';
|
||||||
|
|
||||||
|
function triggerDataUrlDownload(dataUrl, filename) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = filename;
|
||||||
|
a.rel = 'noopener';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function triggerFileDownload(blob, filename) {
|
function triggerFileDownload(blob, filename) {
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
@@ -516,49 +528,36 @@
|
|||||||
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadQrImage(e) {
|
function downloadQrFromCanvas(filename) {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
var filename = downloadBtn.getAttribute('data-filename') || downloadBtn.getAttribute('download') || 'qr.png';
|
|
||||||
var src = downloadSrc || img.currentSrc || img.src;
|
|
||||||
|
|
||||||
function fromCanvas() {
|
|
||||||
try {
|
|
||||||
if (!img.complete || !img.naturalWidth) return false;
|
if (!img.complete || !img.naturalWidth) return false;
|
||||||
|
try {
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = img.naturalWidth;
|
canvas.width = img.naturalWidth;
|
||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
canvas.getContext('2d').drawImage(img, 0, 0);
|
||||||
canvas.toBlob(function (blob) {
|
// toDataURL sync — download stays in the user-gesture (Safari/Chrome)
|
||||||
if (blob) triggerFileDownload(blob, filename);
|
triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
|
||||||
}, 'image/png');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (src) {
|
function downloadQrImage(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
var filename = downloadBtn.getAttribute('data-filename') || 'qr.png';
|
||||||
|
if (downloadQrFromCanvas(filename)) return;
|
||||||
|
|
||||||
|
var src = downloadSrc || img.currentSrc || img.src;
|
||||||
|
if (!src) return;
|
||||||
fetch(src)
|
fetch(src)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error('fetch failed');
|
if (!res.ok) throw new Error('fetch failed');
|
||||||
return res.blob();
|
return res.blob();
|
||||||
})
|
})
|
||||||
.then(function (blob) { triggerFileDownload(blob, filename); })
|
.then(function (blob) { triggerFileDownload(blob, filename); })
|
||||||
.catch(function () {
|
.catch(function () { /* avoid navigating to the image URL */ });
|
||||||
if (!fromCanvas()) {
|
|
||||||
var a = document.createElement('a');
|
|
||||||
a.href = src;
|
|
||||||
a.download = filename;
|
|
||||||
a.rel = 'noopener';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fromCanvas();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLightbox(src, text, alt, filename) {
|
function openLightbox(src, text, alt, filename) {
|
||||||
@@ -567,8 +566,6 @@
|
|||||||
img.alt = alt || text || 'QR';
|
img.alt = alt || text || 'QR';
|
||||||
caption.textContent = text || '';
|
caption.textContent = text || '';
|
||||||
downloadSrc = src;
|
downloadSrc = src;
|
||||||
downloadBtn.href = '#';
|
|
||||||
downloadBtn.setAttribute('download', safeName);
|
|
||||||
downloadBtn.setAttribute('data-filename', safeName);
|
downloadBtn.setAttribute('data-filename', safeName);
|
||||||
downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
|
downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
|
||||||
box.classList.add('open');
|
box.classList.add('open');
|
||||||
@@ -615,6 +612,16 @@
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
function triggerDataUrlDownload(dataUrl, filename) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = filename;
|
||||||
|
a.rel = 'noopener';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function triggerFileDownload(blob, filename) {
|
function triggerFileDownload(blob, filename) {
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
@@ -630,16 +637,13 @@
|
|||||||
function pngFromCard(link, filename) {
|
function pngFromCard(link, filename) {
|
||||||
var card = link.closest('.user-card');
|
var card = link.closest('.user-card');
|
||||||
var qrImg = card && card.querySelector('img[data-lightbox-src], .qr-block img');
|
var qrImg = card && card.querySelector('img[data-lightbox-src], .qr-block img');
|
||||||
if (!qrImg) return false;
|
if (!qrImg || !qrImg.complete || !qrImg.naturalWidth) return false;
|
||||||
try {
|
try {
|
||||||
if (!qrImg.complete || !qrImg.naturalWidth) return false;
|
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = qrImg.naturalWidth;
|
canvas.width = qrImg.naturalWidth;
|
||||||
canvas.height = qrImg.naturalHeight;
|
canvas.height = qrImg.naturalHeight;
|
||||||
canvas.getContext('2d').drawImage(qrImg, 0, 0);
|
canvas.getContext('2d').drawImage(qrImg, 0, 0);
|
||||||
canvas.toBlob(function (blob) {
|
triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
|
||||||
if (blob) triggerFileDownload(blob, filename);
|
|
||||||
}, 'image/png');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -655,18 +659,13 @@
|
|||||||
var fallbackText = link.getAttribute('data-text');
|
var fallbackText = link.getAttribute('data-text');
|
||||||
var isPng = /\.png$/i.test(filename);
|
var isPng = /\.png$/i.test(filename);
|
||||||
|
|
||||||
function fallback() {
|
// Sync paths first — async fetch loses the user-gesture in Safari/Chrome
|
||||||
|
if (isPng && pngFromCard(link, filename)) return;
|
||||||
if (fallbackText !== null) {
|
if (fallbackText !== null) {
|
||||||
triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
|
triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPng) pngFromCard(link, filename);
|
if (!href) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!href) {
|
|
||||||
fallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(href)
|
fetch(href)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
@@ -674,7 +673,7 @@
|
|||||||
return res.blob();
|
return res.blob();
|
||||||
})
|
})
|
||||||
.then(function (blob) { triggerFileDownload(blob, filename); })
|
.then(function (blob) { triggerFileDownload(blob, filename); })
|
||||||
.catch(fallback);
|
.catch(function () { /* do not navigate to the file URL */ });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user