feat(pdf): add print FAB + fix color-mix via clean clone approach
- Added secondary FAB button 'Печать' with printer icon (window.print) - Created printer.svg icon for the print button - Added .fab-secondary__ico--print CSS class - Fixed html2pdf color-mix() issue: buildCleanClone() creates a deep clone with all computed styles inlined, all stylesheets removed, and pseudo-elements converted to real DOM elements before passing to html2pdf - Two separate handlers: pdfBtn for download, printBtn for print dialog
This commit is contained in:
1 parent
319c23e69e
commit
e44a72f559
5 files changed
+286
-94
No files matched your search
+136
-46
@@ -841,51 +841,152 @@
|
||||
{{> cookie-notice}}
|
||||
<script>
|
||||
(function () {
|
||||
var btn = document.querySelector('[data-resume-pdf-btn]');
|
||||
if (!btn) return;
|
||||
var pdfBtn = document.querySelector('[data-resume-pdf-btn]');
|
||||
var printBtn = document.querySelector('[data-resume-print-btn]');
|
||||
if (!pdfBtn && !printBtn) return;
|
||||
|
||||
var busy = false;
|
||||
var CDN = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.2/html2pdf.bundle.min.js';
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
btn.setAttribute('data-loading', 'true');
|
||||
function loadScript(src) {
|
||||
return new Promise(function (ok, fail) {
|
||||
if (window.html2pdf) { ok(); return; }
|
||||
var s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.onload = ok;
|
||||
s.onerror = fail;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
var doc = document.querySelector('.resume-doc');
|
||||
if (!doc) { busy = false; btn.removeAttribute('data-loading'); return; }
|
||||
function toDataURL(img) {
|
||||
return new Promise(function (res) {
|
||||
try {
|
||||
var c = document.createElement('canvas');
|
||||
c.width = img.naturalWidth;
|
||||
c.height = img.naturalHeight;
|
||||
c.getContext('2d').drawImage(img, 0, 0);
|
||||
res(c.toDataURL('image/webp'));
|
||||
} catch (e) { res(null); }
|
||||
});
|
||||
}
|
||||
|
||||
function preparePhoto(doc) {
|
||||
var photo = doc.querySelector('.resume-photo');
|
||||
var origSrc = photo ? photo.getAttribute('src') : null;
|
||||
if (!photo) return Promise.resolve(null);
|
||||
if (photo.complete && photo.naturalWidth > 0) return toDataURL(photo);
|
||||
return new Promise(function (res) {
|
||||
photo.onload = function () { toDataURL(photo).then(res); };
|
||||
photo.onerror = function () { res(null); };
|
||||
});
|
||||
}
|
||||
|
||||
function toDataURL(img) {
|
||||
return new Promise(function (res) {
|
||||
function buildCleanClone(original) {
|
||||
var clone = original.cloneNode(true);
|
||||
var props = ['color','background-color','background','border-color','border-top-color','border-bottom-color','border-left-color','border-right-color','box-shadow','opacity','font-family','font-size','font-weight','font-style','line-height','letter-spacing','text-align','text-decoration','text-transform','padding','padding-top','padding-bottom','padding-left','padding-right','margin','margin-top','margin-bottom','margin-left','margin-right','display','flex-direction','flex-wrap','align-items','align-self','justify-content','gap','grid-template-columns','grid-column','grid-row','width','height','min-width','min-height','max-width','max-height','position','top','bottom','left','right','z-index','overflow','border-radius','border-width','border-style','white-space','list-style-type','object-fit','visibility','cursor','text-indent','vertical-align','table-layout','border-collapse','border-spacing','empty-cells'];
|
||||
|
||||
var origEls = original.querySelectorAll('*');
|
||||
var cloneEls = clone.querySelectorAll('*');
|
||||
for (var i = 0; i < origEls.length && i < cloneEls.length; i++) {
|
||||
try {
|
||||
var cs = getComputedStyle(origEls[i]);
|
||||
for (var j = 0; j < props.length; j++) {
|
||||
var v = cs.getPropertyValue(props[j]);
|
||||
if (v) cloneEls[i].style.setProperty(props[j], v);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
try {
|
||||
var rootCs = getComputedStyle(original);
|
||||
for (var j = 0; j < props.length; j++) {
|
||||
var v = rootCs.getPropertyValue(props[j]);
|
||||
if (v) clone.style.setProperty(props[j], v);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
var sheets = clone.styleSheets;
|
||||
for (var s = sheets.length - 1; s >= 0; s--) {
|
||||
try {
|
||||
var c = document.createElement('canvas');
|
||||
c.width = img.naturalWidth;
|
||||
c.height = img.naturalHeight;
|
||||
c.getContext('2d').drawImage(img, 0, 0);
|
||||
res(c.toDataURL('image/webp'));
|
||||
} catch (e) { res(null); }
|
||||
var sheet = sheets[s];
|
||||
while (sheet.cssRules.length > 0) sheet.deleteRule(0);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
clone.querySelectorAll('style').forEach(function (el) { el.remove(); });
|
||||
clone.querySelectorAll('link[rel="stylesheet"]').forEach(function (el) { el.remove(); });
|
||||
|
||||
var pseudoProps = ['content','color','background-color','background','border-color','border-top-color','border-bottom-color','border-left-color','border-right-color','box-shadow','opacity','font-family','font-size','font-weight','line-height','letter-spacing','text-align','text-decoration','text-transform','padding','padding-top','padding-bottom','padding-left','padding-right','margin','margin-top','margin-bottom','margin-left','margin-right','display','width','height','border-radius','border-width','border-style','white-space','position','top','bottom','left','right','z-index','overflow','list-style-type','object-fit'];
|
||||
for (var i = 0; i < origEls.length && i < cloneEls.length; i++) {
|
||||
['::before','::after'].forEach(function(pseudo) {
|
||||
try {
|
||||
var cs = getComputedStyle(origEls[i], pseudo);
|
||||
var content = cs.getPropertyValue('content');
|
||||
if (!content || content === 'none' || content === 'normal') return;
|
||||
var el = document.createElement('span');
|
||||
el.style.cssText = 'display:block;margin:0;padding:0;border:none;background:none;';
|
||||
for (var j = 0; j < pseudoProps.length; j++) {
|
||||
var v = cs.getPropertyValue(pseudoProps[j]);
|
||||
if (v && v !== 'normal' && v !== 'auto') el.style.setProperty(pseudoProps[j], v);
|
||||
}
|
||||
el.textContent = content.replace(/^["']|["']$/g, '');
|
||||
if (pseudo === '::before') cloneEls[i].prepend(el);
|
||||
else cloneEls[i].appendChild(el);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
var photoReady;
|
||||
if (photo && photo.complete && photo.naturalWidth > 0) {
|
||||
photoReady = toDataURL(photo);
|
||||
} else if (photo) {
|
||||
photoReady = new Promise(function (res) {
|
||||
photo.onload = function () { toDataURL(photo).then(res); };
|
||||
photo.onerror = function () { res(null); };
|
||||
return clone;
|
||||
}
|
||||
|
||||
if (pdfBtn) {
|
||||
pdfBtn.addEventListener('click', function () {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
pdfBtn.setAttribute('data-loading', 'true');
|
||||
|
||||
var doc = document.querySelector('.resume-doc');
|
||||
if (!doc) { busy = false; pdfBtn.removeAttribute('data-loading'); return; }
|
||||
|
||||
var photo = doc.querySelector('.resume-photo');
|
||||
var origSrc = photo ? photo.getAttribute('src') : null;
|
||||
|
||||
preparePhoto(doc).then(function (dataUrl) {
|
||||
if (dataUrl && photo) photo.setAttribute('src', dataUrl);
|
||||
return document.fonts ? document.fonts.ready : Promise.resolve();
|
||||
})
|
||||
.then(function () {
|
||||
return loadScript(CDN);
|
||||
})
|
||||
.then(function () {
|
||||
var clone = buildCleanClone(doc);
|
||||
return html2pdf().from(clone).set({
|
||||
margin: 10,
|
||||
filename: 'Mamadaev_Resume.pdf',
|
||||
image: { type: 'jpeg', quality: 0.95 },
|
||||
html2canvas: { scale: 2, useCORS: true, backgroundColor: '#fff' },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||
}).save();
|
||||
})
|
||||
.then(function () {
|
||||
if (origSrc && photo) photo.setAttribute('src', origSrc);
|
||||
busy = false;
|
||||
pdfBtn.removeAttribute('data-loading');
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error('[PDF] error:', e);
|
||||
if (origSrc && photo) photo.setAttribute('src', origSrc);
|
||||
busy = false;
|
||||
pdfBtn.removeAttribute('data-loading');
|
||||
});
|
||||
} else {
|
||||
photoReady = Promise.resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (printBtn) {
|
||||
printBtn.addEventListener('click', function () {
|
||||
var doc = document.querySelector('.resume-doc');
|
||||
if (!doc) return;
|
||||
|
||||
photoReady.then(function (dataUrl) {
|
||||
if (dataUrl && photo) photo.setAttribute('src', dataUrl);
|
||||
return document.fonts ? document.fonts.ready : Promise.resolve();
|
||||
})
|
||||
.then(function () {
|
||||
var styles = [];
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach(function (l) {
|
||||
styles.push('<link rel="stylesheet" href="' + l.href + '">');
|
||||
@@ -895,7 +996,7 @@
|
||||
});
|
||||
|
||||
var win = window.open('', '_blank');
|
||||
if (!win) { throw new Error('Popup blocked'); }
|
||||
if (!win) return;
|
||||
win.document.write(
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8">' +
|
||||
styles.join('') +
|
||||
@@ -903,22 +1004,11 @@
|
||||
'</head><body>' + doc.outerHTML + '</body></html>'
|
||||
);
|
||||
win.document.close();
|
||||
|
||||
win.onload = function () {
|
||||
setTimeout(function () {
|
||||
win.print();
|
||||
if (origSrc && photo) photo.setAttribute('src', origSrc);
|
||||
busy = false;
|
||||
btn.removeAttribute('data-loading');
|
||||
}, 500);
|
||||
setTimeout(function () { win.print(); }, 500);
|
||||
};
|
||||
})
|
||||
.catch(function () {
|
||||
if (origSrc && photo) photo.setAttribute('src', origSrc);
|
||||
busy = false;
|
||||
btn.removeAttribute('data-loading');
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
<span class="fab__label">Скачать в PDF</span>
|
||||
<span class="fab__spinner" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="fab fab--secondary" data-resume-print-btn aria-label="Печать резюме">
|
||||
<span class="fab-secondary__ico fab-secondary__ico--print" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
Reference in new issue
Block a user