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:
Local Build committed 2026-08-13 21:20:27 +02:00
1 parent 319c23e69e
commit e44a72f559
5 files changed
+286 -94

No files matched your search

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>

After

Width:  |  Height:  |  Size: 330 B

+6
View File
@@ -960,6 +960,12 @@ a.fab--secondary:visited {
-webkit-mask-image: url("../assets/shared/icons/cv.svg");
}
/* ── Print FAB ── */
.fab-secondary__ico--print {
mask-image: url("../assets/shared/icons/printer.svg");
-webkit-mask-image: url("../assets/shared/icons/printer.svg");
}
.fab--label {
width: auto;
padding: 0.65rem 1.1rem;
+139 -47
View File
@@ -322,8 +322,10 @@
<span class="fab__label">Скачать в PDF</span>
<span class="fab__spinner" aria-hidden="true"></span>
</button>
<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>
<main id="main" class="section section--surface section--tight">
<div class="section__inner resume-doc">
<div class="resume-header">
@@ -972,51 +974,152 @@
<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 + '">');
@@ -1026,7 +1129,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('') +
@@ -1034,22 +1137,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>
+136 -46
View File
@@ -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 -1
View File
@@ -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>