@cyberskill/design 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +1 -1
- package/README.md +4 -4
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/_ds_bundle.js +2 -2
- package/_esm/cs.mjs +2 -2
- package/docs/contrast-report.md +1 -1
- package/docs/conventions.md +2 -2
- package/docs/quality-gates.md +1 -1
- package/docs/release-notes.md +1 -1
- package/docs/vi/contrast-report.md +1 -1
- package/docs/vi/conventions.md +2 -2
- package/docs/vi/quality-gates.md +1 -1
- package/docs/vi/release-notes.md +1 -1
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/templates/_vendor/doc-export.js +536 -59
- package/templates/_vendor/support.js +9 -2
- package/templates/app-shell/support.js +9 -2
- package/templates/article/support.js +9 -2
- package/templates/auth/support.js +9 -2
- package/templates/bod-deck/support.js +9 -2
- package/templates/bod-investor-update/support.js +9 -2
- package/templates/bod-memo/support.js +9 -2
- package/templates/bod-minutes/support.js +9 -2
- package/templates/bod-report/support.js +9 -2
- package/templates/bod-resolution/support.js +9 -2
- package/templates/culture-event-invite/support.js +9 -2
- package/templates/dashboard/support.js +9 -2
- package/templates/delivery-change-note/support.js +9 -2
- package/templates/delivery-kickoff/support.js +9 -2
- package/templates/delivery-qbr-deck/support.js +9 -2
- package/templates/delivery-runbook/support.js +9 -2
- package/templates/delivery-status-email/support.js +9 -2
- package/templates/doc-style-guide/support.js +9 -2
- package/templates/doc-suite-index/support.js +9 -2
- package/templates/doc-templates/support.js +9 -2
- package/templates/email/support.js +9 -2
- package/templates/finance-dunning-email/support.js +9 -2
- package/templates/finance-expense-report/support.js +9 -2
- package/templates/finance-invoice/support.js +9 -2
- package/templates/finance-quote/support.js +9 -2
- package/templates/hr-announcement/support.js +9 -2
- package/templates/hr-interview-kit/support.js +9 -2
- package/templates/hr-offboarding/support.js +9 -2
- package/templates/hr-performance-review/support.js +9 -2
- package/templates/hr-pip/support.js +9 -2
- package/templates/legal-msa/support.js +9 -2
- package/templates/legal-sow/support.js +9 -2
- package/templates/marketing-campaign-brief/support.js +9 -2
- package/templates/marketing-case-study/support.js +9 -2
- package/templates/marketing-launch/support.js +9 -2
- package/templates/marketing-newsletter/support.js +9 -2
- package/templates/marketing-page/support.js +9 -2
- package/templates/marketing-press-release/support.js +9 -2
- package/templates/marketing-social-kit/support.js +9 -2
- package/templates/sales-one-pager/support.js +9 -2
- package/templates/sales-proposal-deck/support.js +9 -2
- package/templates/settings/support.js +9 -2
- package/templates/slide-deck/support.js +9 -2
- package/templates/team-meeting-agenda/support.js +9 -2
- package/templates/tech-incident-report/support.js +9 -2
- package/templates/tech-release-notes/support.js +9 -2
- package/templates/tech-retro/support.js +9 -2
- package/templates/tech-rfc/support.js +9 -2
- package/templates/vn-appointment-transfer/support.js +9 -2
- package/templates/vn-certificate-of-employment/support.js +9 -2
- package/templates/vn-code-of-conduct/support.js +9 -2
- package/templates/vn-compensation-regulation/support.js +9 -2
- package/templates/vn-contract-amendment/support.js +9 -2
- package/templates/vn-contractor-agreement/support.js +9 -2
- package/templates/vn-data-protection-policy/support.js +9 -2
- package/templates/vn-disciplinary-case-file/support.js +9 -2
- package/templates/vn-disciplinary-schedule/support.js +9 -2
- package/templates/vn-employee-request/support.js +9 -2
- package/templates/vn-exit-handover/support.js +9 -2
- package/templates/vn-foreign-employee-pack/support.js +9 -2
- package/templates/vn-framework-agreement/VnFrameworkAgreement.dc.html +1 -3
- package/templates/vn-framework-agreement/support.js +9 -2
- package/templates/vn-grassroots-democracy/support.js +9 -2
- package/templates/vn-infosec-policy/support.js +9 -2
- package/templates/vn-internal-labor-regulations/support.js +9 -2
- package/templates/vn-internship-agreement/support.js +9 -2
- package/templates/vn-job-description/support.js +9 -2
- package/templates/vn-labor-contract/support.js +9 -2
- package/templates/vn-labour-management-book/support.js +9 -2
- package/templates/vn-mutual-nda/support.js +9 -2
- package/templates/vn-ndnca-ip/support.js +9 -2
- package/templates/vn-offer-letter/support.js +9 -2
- package/templates/vn-onboarding-checklist/support.js +9 -2
- package/templates/vn-pdpl-consent/support.js +9 -2
- package/templates/vn-performance-evaluation-regulation/support.js +9 -2
- package/templates/vn-performance-review-form/support.js +9 -2
- package/templates/vn-phantom-stock/support.js +9 -2
- package/templates/vn-probation-contract/support.js +9 -2
- package/templates/vn-receipt-acknowledgement/support.js +9 -2
- package/templates/vn-remote-work-policy/support.js +9 -2
- package/templates/vn-resignation-letter/support.js +9 -2
- package/templates/vn-salary-scale/support.js +9 -2
- package/templates/vn-sow-appendix/support.js +9 -2
- package/templates/vn-termination-decision/support.js +9 -2
- package/templates/vn-total-rewards-appendix/support.js +9 -2
- package/templates/vn-training-commitment/support.js +9 -2
- package/templates/vn-travel-expense-policy/support.js +9 -2
- package/tokens/element-seeds.json +1 -1
- package/tokens/native/CSTokens.kt +1 -1
- package/tokens/native/CSTokens.swift +1 -1
- package/tokens/native/cs_tokens.dart +1 -1
- package/tokens/provenance.json +6 -6
- package/tokens/tokens.dtcg.json +1 -1
- package/tokens/tokens.js +2 -2
- package/tokens/tokens.json +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CyberSkill document export —
|
|
2
|
+
* CyberSkill document export — one Download menu (PDF | DOCX).
|
|
3
|
+
* PDF/DOCX are built from the live `.cs-sheet` so attachments match the HTML preview.
|
|
3
4
|
* Loaded by ds-base.js when <meta name="omelette-owns-print"> is present.
|
|
4
5
|
* No CDN; ZIP via CompressionStream (deflate-raw).
|
|
5
6
|
*/
|
|
6
7
|
(() => {
|
|
7
8
|
const UMBER = '45210E';
|
|
8
|
-
const A4 = { w: 11906, h: 16838, name: 'A4' };
|
|
9
|
-
const LETTER = { w: 12240, h: 15840, name: 'Letter' };
|
|
10
|
-
|
|
9
|
+
const A4 = { w: 11906, h: 16838, name: 'A4', ptW: 595.28, ptH: 841.89 };
|
|
10
|
+
const LETTER = { w: 12240, h: 15840, name: 'Letter', ptW: 612, ptH: 792 };
|
|
11
11
|
function pageSizeFromCss() {
|
|
12
12
|
const styles = [...document.querySelectorAll('style')].map((s) => s.textContent || '').join('\n');
|
|
13
13
|
if (/@page\s*\{[^}]*size:\s*letter/i.test(styles)) return LETTER;
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
|
|
34
34
|
async function deflateRaw(u8) {
|
|
35
35
|
if (typeof CompressionStream === 'undefined') {
|
|
36
|
-
// Store uncompressed (ZIP method 0) — still a valid OOXML package.
|
|
37
36
|
return { data: u8, method: 0 };
|
|
38
37
|
}
|
|
39
38
|
const cs = new CompressionStream('deflate-raw');
|
|
@@ -45,7 +44,6 @@
|
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
async function buildZip(files) {
|
|
48
|
-
// files: [{ name, bytes: Uint8Array }]
|
|
49
47
|
const enc = new TextEncoder();
|
|
50
48
|
const locals = [];
|
|
51
49
|
const centrals = [];
|
|
@@ -106,8 +104,7 @@
|
|
|
106
104
|
ev.setUint32(16, offset, true);
|
|
107
105
|
ev.setUint16(20, 0, true);
|
|
108
106
|
|
|
109
|
-
const total =
|
|
110
|
-
locals.reduce((n, b) => n + b.length, 0) + centralSize + end.length;
|
|
107
|
+
const total = locals.reduce((n, b) => n + b.length, 0) + centralSize + end.length;
|
|
111
108
|
const out = new Uint8Array(total);
|
|
112
109
|
let o = 0;
|
|
113
110
|
for (const b of locals) {
|
|
@@ -122,21 +119,29 @@
|
|
|
122
119
|
return out;
|
|
123
120
|
}
|
|
124
121
|
|
|
122
|
+
function isChrome(el) {
|
|
123
|
+
return el.getAttribute && el.getAttribute('data-omelette-chrome') != null;
|
|
124
|
+
}
|
|
125
|
+
|
|
125
126
|
function textRuns(el) {
|
|
126
127
|
const parts = [];
|
|
127
128
|
const walk = (node, italic, bold, color) => {
|
|
128
129
|
if (node.nodeType === 3) {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
130
|
+
const raw = node.textContent || '';
|
|
131
|
+
if (!raw) return;
|
|
132
|
+
if (!raw.replace(/\s+/g, '').length) {
|
|
133
|
+
if (parts.length) parts.push({ t: ' ', italic, bold, color });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
parts.push({ t: raw.replace(/\s+/g, ' '), italic, bold, color });
|
|
131
137
|
return;
|
|
132
138
|
}
|
|
133
139
|
if (node.nodeType !== 1) return;
|
|
134
|
-
if (node
|
|
140
|
+
if (isChrome(node)) return;
|
|
135
141
|
const tag = node.tagName.toLowerCase();
|
|
136
142
|
if (tag === 'script' || tag === 'style' || tag === 'noscript') return;
|
|
137
143
|
const st = window.getComputedStyle(node);
|
|
138
|
-
const nextItalic =
|
|
139
|
-
italic || st.fontStyle === 'italic' || tag === 'i' || tag === 'em';
|
|
144
|
+
const nextItalic = italic || st.fontStyle === 'italic' || tag === 'i' || tag === 'em';
|
|
140
145
|
const nextBold =
|
|
141
146
|
bold ||
|
|
142
147
|
Number(st.fontWeight) >= 600 ||
|
|
@@ -150,7 +155,21 @@
|
|
|
150
155
|
for (const c of node.childNodes) walk(c, nextItalic, nextBold, nextColor);
|
|
151
156
|
};
|
|
152
157
|
walk(el, false, false, null);
|
|
153
|
-
|
|
158
|
+
const merged = [];
|
|
159
|
+
for (const r of parts) {
|
|
160
|
+
const prev = merged[merged.length - 1];
|
|
161
|
+
if (
|
|
162
|
+
prev &&
|
|
163
|
+
prev.italic === r.italic &&
|
|
164
|
+
prev.bold === r.bold &&
|
|
165
|
+
prev.color === r.color
|
|
166
|
+
) {
|
|
167
|
+
prev.t += r.t;
|
|
168
|
+
} else {
|
|
169
|
+
merged.push({ ...r });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return merged;
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
function runsToXml(runs) {
|
|
@@ -177,6 +196,24 @@
|
|
|
177
196
|
return `<w:p>${pPr}${runsToXml(runs)}</w:p>`;
|
|
178
197
|
}
|
|
179
198
|
|
|
199
|
+
function cellInnerXml(cell, fillWhite) {
|
|
200
|
+
const blocks = [...cell.children].filter((c) =>
|
|
201
|
+
/^(DIV|P|UL|OL|H1|H2|H3|H4|H5|H6)$/.test(c.tagName),
|
|
202
|
+
);
|
|
203
|
+
const paint = (runs) =>
|
|
204
|
+
runs.map((r) => (fillWhite ? { ...r, color: r.color || 'FFFFFF' } : r));
|
|
205
|
+
if (blocks.length && blocks.length === cell.children.length) {
|
|
206
|
+
const paras = blocks
|
|
207
|
+
.map((b) => {
|
|
208
|
+
const runs = paint(textRuns(b));
|
|
209
|
+
return runs.length ? `<w:p>${runsToXml(runs)}</w:p>` : '';
|
|
210
|
+
})
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
return paras.join('') || `<w:p>${runsToXml([])}</w:p>`;
|
|
213
|
+
}
|
|
214
|
+
return `<w:p>${runsToXml(paint(textRuns(cell)))}</w:p>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
180
217
|
function tableXml(table) {
|
|
181
218
|
const rows = [...table.querySelectorAll(':scope > tbody > tr, :scope > thead > tr, :scope > tr')];
|
|
182
219
|
if (!rows.length) return '';
|
|
@@ -194,15 +231,42 @@
|
|
|
194
231
|
xml += '<w:tr>';
|
|
195
232
|
for (const cell of tr.children) {
|
|
196
233
|
const bg = window.getComputedStyle(cell).backgroundColor;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
234
|
+
const umberFill = bg && /rgb\(\s*69\s*,\s*33\s*,\s*14\s*\)/.test(bg);
|
|
235
|
+
const shade = umberFill
|
|
236
|
+
? `<w:tcPr><w:shd w:val="clear" w:fill="${UMBER}"/><w:tcW w:w="${colW}" w:type="dxa"/></w:tcPr>`
|
|
237
|
+
: `<w:tcPr><w:tcW w:w="${colW}" w:type="dxa"/></w:tcPr>`;
|
|
238
|
+
xml += `<w:tc>${shade}${cellInnerXml(cell, umberFill)}</w:tc>`;
|
|
239
|
+
}
|
|
240
|
+
xml += '</w:tr>';
|
|
241
|
+
}
|
|
242
|
+
xml += '</w:tbl>';
|
|
243
|
+
return xml;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function gridColCount(el) {
|
|
247
|
+
const st = window.getComputedStyle(el);
|
|
248
|
+
if (st.display !== 'grid' && st.display !== 'inline-grid') return 0;
|
|
249
|
+
const tpl = st.gridTemplateColumns || '';
|
|
250
|
+
const parts = tpl.split(/\s+/).filter(Boolean);
|
|
251
|
+
return parts.length >= 2 ? parts.length : 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function gridAsTable(el) {
|
|
255
|
+
const cols = gridColCount(el);
|
|
256
|
+
if (!cols) return '';
|
|
257
|
+
const kids = [...el.children].filter((c) => c.nodeType === 1 && !isChrome(c));
|
|
258
|
+
if (kids.length < 2) return '';
|
|
259
|
+
const colW = Math.floor(9000 / cols);
|
|
260
|
+
let xml = `<w:tbl><w:tblPr><w:tblW w:w="5000" w:type="pct"/><w:tblBorders>
|
|
261
|
+
<w:top w:val="nil"/><w:left w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/>
|
|
262
|
+
<w:insideH w:val="nil"/><w:insideV w:val="nil"/>
|
|
263
|
+
</w:tblBorders></w:tblPr><w:tblGrid>${'<w:gridCol w:w="' + colW + '"/>'.repeat(cols)}</w:tblGrid>`;
|
|
264
|
+
for (let i = 0; i < kids.length; i += cols) {
|
|
265
|
+
xml += '<w:tr>';
|
|
266
|
+
for (let c = 0; c < cols; c++) {
|
|
267
|
+
const src = kids[i + c];
|
|
268
|
+
const inner = src ? cellInnerXml(src, false) : `<w:p>${runsToXml([])}</w:p>`;
|
|
269
|
+
xml += `<w:tc><w:tcPr><w:tcW w:w="${colW}" w:type="dxa"/></w:tcPr>${inner}</w:tc>`;
|
|
206
270
|
}
|
|
207
271
|
xml += '</w:tr>';
|
|
208
272
|
}
|
|
@@ -212,11 +276,10 @@
|
|
|
212
276
|
|
|
213
277
|
function sheetBodyXml(sheet) {
|
|
214
278
|
const blocks = [];
|
|
215
|
-
const kids = [...sheet.children];
|
|
216
279
|
const visit = (nodes) => {
|
|
217
280
|
for (const el of nodes) {
|
|
218
281
|
if (el.nodeType !== 1) continue;
|
|
219
|
-
if (el
|
|
282
|
+
if (isChrome(el)) continue;
|
|
220
283
|
const tag = el.tagName.toLowerCase();
|
|
221
284
|
if (tag === 'table') {
|
|
222
285
|
blocks.push(tableXml(el));
|
|
@@ -226,8 +289,12 @@
|
|
|
226
289
|
blocks.push(paraXml(el, 'Heading1'));
|
|
227
290
|
continue;
|
|
228
291
|
}
|
|
292
|
+
const gridXml = gridAsTable(el);
|
|
293
|
+
if (gridXml) {
|
|
294
|
+
blocks.push(gridXml);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
229
297
|
if (tag === 'p' || tag === 'div' || tag === 'section' || tag === 'li') {
|
|
230
|
-
// Prefer leaf paragraphs; if a div only wraps structure, descend.
|
|
231
298
|
const hasBlock = [...el.children].some((c) =>
|
|
232
299
|
/^(DIV|TABLE|SECTION|UL|OL|H1|H2|H3|H4|H5|H6)$/.test(c.tagName),
|
|
233
300
|
);
|
|
@@ -246,21 +313,21 @@
|
|
|
246
313
|
visit([...el.children]);
|
|
247
314
|
}
|
|
248
315
|
};
|
|
249
|
-
visit(
|
|
316
|
+
visit([...sheet.children]);
|
|
250
317
|
return blocks.filter(Boolean).join('');
|
|
251
318
|
}
|
|
252
319
|
|
|
253
|
-
function headerFooterText(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
320
|
+
function headerFooterText() {
|
|
321
|
+
return {
|
|
322
|
+
header: 'CyberSkill · Hiện Thực Hoá Ý Chí',
|
|
323
|
+
footer: 'CyberSkill · MST 0316475200 · info@cyberskill.world',
|
|
324
|
+
};
|
|
258
325
|
}
|
|
259
326
|
|
|
260
327
|
async function buildDocxBlob(sheet) {
|
|
261
328
|
const page = pageSizeFromCss();
|
|
262
329
|
const enc = new TextEncoder();
|
|
263
|
-
const { header, footer } = headerFooterText(
|
|
330
|
+
const { header, footer } = headerFooterText();
|
|
264
331
|
const body = sheetBodyXml(sheet);
|
|
265
332
|
|
|
266
333
|
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
@@ -306,8 +373,7 @@
|
|
|
306
373
|
</w:hdr>`;
|
|
307
374
|
|
|
308
375
|
const footerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
309
|
-
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
310
|
-
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml">
|
|
376
|
+
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
311
377
|
<w:p>
|
|
312
378
|
<w:r><w:rPr><w:sz w:val="16"/><w:color w:val="6B5D4F"/></w:rPr>
|
|
313
379
|
<w:t xml:space="preserve">${xmlEscape(footer)} · </w:t></w:r>
|
|
@@ -345,17 +411,352 @@
|
|
|
345
411
|
});
|
|
346
412
|
}
|
|
347
413
|
|
|
348
|
-
function
|
|
414
|
+
function cssVarBlock() {
|
|
415
|
+
const cs = getComputedStyle(document.documentElement);
|
|
416
|
+
const parts = [];
|
|
417
|
+
for (let i = 0; i < cs.length; i++) {
|
|
418
|
+
const p = cs[i];
|
|
419
|
+
if (p.startsWith('--')) parts.push(`${p}:${cs.getPropertyValue(p)}`);
|
|
420
|
+
}
|
|
421
|
+
return parts.length ? `:root{${parts.join(';')}}` : '';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function dataUrlToBytes(dataUrl) {
|
|
425
|
+
const b64 = dataUrl.split(',')[1] || '';
|
|
426
|
+
const bin = atob(b64);
|
|
427
|
+
const out = new Uint8Array(bin.length);
|
|
428
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function canvasHasInk(ctx, w, h) {
|
|
433
|
+
const stepX = Math.max(1, Math.floor(w / 12));
|
|
434
|
+
const stepY = Math.max(1, Math.floor(h / 16));
|
|
435
|
+
for (let y = 8; y < h; y += stepY) {
|
|
436
|
+
for (let x = 8; x < w; x += stepX) {
|
|
437
|
+
const p = ctx.getImageData(x, y, 1, 1).data;
|
|
438
|
+
if (p[0] < 248 || p[1] < 248 || p[2] < 248) return true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function loadImage(url) {
|
|
445
|
+
return new Promise((resolve, reject) => {
|
|
446
|
+
const img = new Image();
|
|
447
|
+
img.onload = () => resolve(img);
|
|
448
|
+
img.onerror = () => reject(new Error('sheet-image-failed'));
|
|
449
|
+
img.src = url;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function concatBytes(parts) {
|
|
454
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
455
|
+
const out = new Uint8Array(total);
|
|
456
|
+
let o = 0;
|
|
457
|
+
for (const p of parts) {
|
|
458
|
+
out.set(p, o);
|
|
459
|
+
o += p.length;
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function jpegPagesToPdf(pages, mediaW, mediaH) {
|
|
465
|
+
const enc = new TextEncoder();
|
|
466
|
+
const nl = enc.encode('\n');
|
|
467
|
+
const chunks = [enc.encode('%PDF-1.4\n')];
|
|
468
|
+
const offsets = [0];
|
|
469
|
+
const pushObj = (id, dict, stream) => {
|
|
470
|
+
offsets[id] = chunks.reduce((n, c) => n + c.length, 0);
|
|
471
|
+
chunks.push(enc.encode(`${id} 0 obj\n`));
|
|
472
|
+
if (stream) {
|
|
473
|
+
chunks.push(enc.encode(dict));
|
|
474
|
+
chunks.push(nl);
|
|
475
|
+
chunks.push(enc.encode('stream\n'));
|
|
476
|
+
chunks.push(stream);
|
|
477
|
+
chunks.push(enc.encode('\nendstream\nendobj\n'));
|
|
478
|
+
} else {
|
|
479
|
+
chunks.push(enc.encode(dict));
|
|
480
|
+
chunks.push(enc.encode('\nendobj\n'));
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const pageIds = [];
|
|
485
|
+
let nextId = 3;
|
|
486
|
+
for (const page of pages) {
|
|
487
|
+
const pageId = nextId++;
|
|
488
|
+
const imgId = nextId++;
|
|
489
|
+
const contentId = nextId++;
|
|
490
|
+
pageIds.push(pageId);
|
|
491
|
+
const content = `q ${mediaW.toFixed(2)} 0 0 ${mediaH.toFixed(2)} 0 0 cm /Im0 Do Q\n`;
|
|
492
|
+
pushObj(
|
|
493
|
+
pageId,
|
|
494
|
+
`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${mediaW.toFixed(2)} ${mediaH.toFixed(2)}] /Resources << /XObject << /Im0 ${imgId} 0 R >> >> /Contents ${contentId} 0 R >>`,
|
|
495
|
+
);
|
|
496
|
+
pushObj(
|
|
497
|
+
imgId,
|
|
498
|
+
`<< /Type /XObject /Subtype /Image /Width ${page.w} /Height ${page.h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${page.jpeg.length} >>`,
|
|
499
|
+
page.jpeg,
|
|
500
|
+
);
|
|
501
|
+
const contentBytes = enc.encode(content);
|
|
502
|
+
pushObj(contentId, `<< /Length ${contentBytes.length} >>`, contentBytes);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
pushObj(1, '<< /Type /Catalog /Pages 2 0 R >>');
|
|
506
|
+
pushObj(
|
|
507
|
+
2,
|
|
508
|
+
`<< /Type /Pages /Kids [${pageIds.map((id) => id + ' 0 R').join(' ')}] /Count ${pageIds.length} >>`,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const body = concatBytes(chunks);
|
|
512
|
+
const xrefAt = body.length;
|
|
513
|
+
let xref = `xref\n0 ${nextId}\n0000000000 65535 f \n`;
|
|
514
|
+
for (let i = 1; i < nextId; i++) {
|
|
515
|
+
xref += String(offsets[i] || 0).padStart(10, '0') + ' 00000 n \n';
|
|
516
|
+
}
|
|
517
|
+
const trailer = `trailer\n<< /Size ${nextId} /Root 1 0 R >>\nstartxref\n${xrefAt}\n%%EOF\n`;
|
|
518
|
+
return concatBytes([body, enc.encode(xref), enc.encode(trailer)]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function relBox(el, origin, sheet) {
|
|
522
|
+
const r = el.getBoundingClientRect();
|
|
523
|
+
return {
|
|
524
|
+
x: r.left - origin.left + sheet.scrollLeft,
|
|
525
|
+
y: r.top - origin.top + sheet.scrollTop,
|
|
526
|
+
w: r.width,
|
|
527
|
+
h: r.height,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function paintBox(ctx, el, origin, sheet) {
|
|
532
|
+
const cs = getComputedStyle(el);
|
|
533
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || Number(cs.opacity) === 0) return;
|
|
534
|
+
const b = relBox(el, origin, sheet);
|
|
535
|
+
const bg = cs.backgroundColor;
|
|
536
|
+
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
|
537
|
+
ctx.fillStyle = bg;
|
|
538
|
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
539
|
+
}
|
|
540
|
+
const sides = [
|
|
541
|
+
['Top', b.x, b.y, b.x + b.w, b.y],
|
|
542
|
+
['Right', b.x + b.w, b.y, b.x + b.w, b.y + b.h],
|
|
543
|
+
['Bottom', b.x, b.y + b.h, b.x + b.w, b.y + b.h],
|
|
544
|
+
['Left', b.x, b.y, b.x, b.y + b.h],
|
|
545
|
+
];
|
|
546
|
+
for (const [side, x1, y1, x2, y2] of sides) {
|
|
547
|
+
const w = parseFloat(cs[`border${side}Width`]) || 0;
|
|
548
|
+
if (w <= 0 || cs[`border${side}Style`] === 'none') continue;
|
|
549
|
+
ctx.strokeStyle = cs[`border${side}Color`];
|
|
550
|
+
ctx.lineWidth = w;
|
|
551
|
+
ctx.beginPath();
|
|
552
|
+
ctx.moveTo(x1, y1);
|
|
553
|
+
ctx.lineTo(x2, y2);
|
|
554
|
+
ctx.stroke();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function paintTextNode(ctx, node, origin, sheet) {
|
|
559
|
+
const raw = node.textContent || '';
|
|
560
|
+
if (!raw.replace(/\s+/g, '').length) return;
|
|
561
|
+
const parent = node.parentElement;
|
|
562
|
+
if (!parent) return;
|
|
563
|
+
const cs = getComputedStyle(parent);
|
|
564
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') return;
|
|
565
|
+
ctx.save();
|
|
566
|
+
ctx.fillStyle = cs.color || '#000';
|
|
567
|
+
ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
|
|
568
|
+
ctx.textBaseline = 'top';
|
|
569
|
+
const range = document.createRange();
|
|
570
|
+
const parts = raw.split(/(\s+)/);
|
|
571
|
+
let offset = 0;
|
|
572
|
+
for (const part of parts) {
|
|
573
|
+
const next = offset + part.length;
|
|
574
|
+
if (part.replace(/\s+/g, '').length) {
|
|
575
|
+
try {
|
|
576
|
+
range.setStart(node, offset);
|
|
577
|
+
range.setEnd(node, Math.min(next, raw.length));
|
|
578
|
+
const r = range.getBoundingClientRect();
|
|
579
|
+
if (r.width || r.height) {
|
|
580
|
+
ctx.fillText(
|
|
581
|
+
part,
|
|
582
|
+
r.left - origin.left + sheet.scrollLeft,
|
|
583
|
+
r.top - origin.top + sheet.scrollTop,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
} catch (_) {
|
|
587
|
+
/* invalid offsets on some hosts */
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
offset = next;
|
|
591
|
+
}
|
|
592
|
+
ctx.restore();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function paintSvg(ctx, svg, origin, sheet) {
|
|
596
|
+
const b = relBox(svg, origin, sheet);
|
|
597
|
+
if (b.w < 1 || b.h < 1) return;
|
|
598
|
+
try {
|
|
599
|
+
const xml = new XMLSerializer().serializeToString(svg);
|
|
600
|
+
if (/foreignObject/i.test(xml)) return;
|
|
601
|
+
const img = await loadImage('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(xml));
|
|
602
|
+
ctx.drawImage(img, b.x, b.y, b.w, b.h);
|
|
603
|
+
} catch (_) {
|
|
604
|
+
/* skip logo if SVG paint fails */
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function paintSheet(sheet, scale) {
|
|
609
|
+
const origin = sheet.getBoundingClientRect();
|
|
610
|
+
const width = Math.max(sheet.scrollWidth, sheet.clientWidth, 1);
|
|
611
|
+
const height = Math.max(sheet.scrollHeight, sheet.clientHeight, 1);
|
|
612
|
+
const canvas = document.createElement('canvas');
|
|
613
|
+
canvas.width = Math.max(1, Math.round(width * scale));
|
|
614
|
+
canvas.height = Math.max(1, Math.round(height * scale));
|
|
615
|
+
const ctx = canvas.getContext('2d');
|
|
616
|
+
ctx.scale(scale, scale);
|
|
617
|
+
ctx.fillStyle = '#ffffff';
|
|
618
|
+
ctx.fillRect(0, 0, width, height);
|
|
619
|
+
const svgs = [];
|
|
620
|
+
const walk = (node) => {
|
|
621
|
+
if (node.nodeType === 1) {
|
|
622
|
+
if (isChrome(node)) return;
|
|
623
|
+
const tag = node.tagName.toLowerCase();
|
|
624
|
+
if (tag === 'script' || tag === 'style' || tag === 'noscript') return;
|
|
625
|
+
if (tag === 'svg') {
|
|
626
|
+
paintBox(ctx, node, origin, sheet);
|
|
627
|
+
svgs.push(node);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
paintBox(ctx, node, origin, sheet);
|
|
631
|
+
for (const c of node.childNodes) walk(c);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (node.nodeType === 3) paintTextNode(ctx, node, origin, sheet);
|
|
635
|
+
};
|
|
636
|
+
walk(sheet);
|
|
637
|
+
for (const svg of svgs) await paintSvg(ctx, svg, origin, sheet);
|
|
638
|
+
return { canvas, width, height, ctx };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function rasterizeSheet(sheet) {
|
|
642
|
+
const page = pageSizeFromCss();
|
|
643
|
+
const scale = 2;
|
|
644
|
+
const painted = await paintSheet(sheet, scale);
|
|
645
|
+
const { canvas, width, height, ctx } = painted;
|
|
646
|
+
let ink = true;
|
|
647
|
+
try {
|
|
648
|
+
ink = canvasHasInk(ctx, canvas.width, Math.min(canvas.height, 400));
|
|
649
|
+
} catch (_) {
|
|
650
|
+
ink = true;
|
|
651
|
+
}
|
|
652
|
+
if (!ink) throw new Error('blank-raster');
|
|
653
|
+
const pageH = Math.round(width * (page.h / page.w) * scale);
|
|
654
|
+
const pages = [];
|
|
655
|
+
let y = 0;
|
|
656
|
+
while (y < canvas.height - 1) {
|
|
657
|
+
const ch = Math.min(pageH, canvas.height - y);
|
|
658
|
+
const slice = document.createElement('canvas');
|
|
659
|
+
slice.width = canvas.width;
|
|
660
|
+
slice.height = Math.max(1, ch);
|
|
661
|
+
const sctx = slice.getContext('2d');
|
|
662
|
+
sctx.fillStyle = '#ffffff';
|
|
663
|
+
sctx.fillRect(0, 0, slice.width, slice.height);
|
|
664
|
+
sctx.drawImage(canvas, 0, y, canvas.width, ch, 0, 0, canvas.width, ch);
|
|
665
|
+
pages.push({
|
|
666
|
+
jpeg: dataUrlToBytes(slice.toDataURL('image/jpeg', 0.92)),
|
|
667
|
+
w: slice.width,
|
|
668
|
+
h: slice.height,
|
|
669
|
+
});
|
|
670
|
+
y += pageH;
|
|
671
|
+
if (pages.length > 40) break;
|
|
672
|
+
}
|
|
673
|
+
if (!pages.length) throw new Error('no-pages');
|
|
674
|
+
return jpegPagesToPdf(pages, page.ptW, page.ptH);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function buildPdfBlob(sheet) {
|
|
678
|
+
const bytes = await rasterizeSheet(sheet);
|
|
679
|
+
return new Blob([bytes], { type: 'application/pdf' });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function printPdf() {
|
|
683
|
+
const sheet = document.querySelector('.cs-sheet') || document.body;
|
|
684
|
+
const page = pageSizeFromCss();
|
|
685
|
+
const size = page.name === 'Letter' ? 'letter' : 'A4';
|
|
686
|
+
const clone = sheet.cloneNode(true);
|
|
687
|
+
clone.querySelectorAll('[data-omelette-chrome]').forEach((n) => n.remove());
|
|
688
|
+
const links = [...document.querySelectorAll('link[rel="stylesheet"]')]
|
|
689
|
+
.map((l) => `<link rel="stylesheet" href="${xmlEscape(l.href)}">`)
|
|
690
|
+
.join('');
|
|
691
|
+
const styles = [...document.querySelectorAll('style')]
|
|
692
|
+
.filter((s) => s.getAttribute('data-omelette-chrome') == null)
|
|
693
|
+
.map((s) => `<style>${s.textContent || ''}</style>`)
|
|
694
|
+
.join('');
|
|
695
|
+
const title = xmlEscape(document.title || 'document');
|
|
696
|
+
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${title}</title>
|
|
697
|
+
${links}${styles}
|
|
698
|
+
<style>${cssVarBlock()}
|
|
699
|
+
@page{size:${size};margin:16mm 14mm}
|
|
700
|
+
html,body{margin:0;padding:0;background:#fff!important;height:auto!important;max-height:none!important;overflow:visible!important}
|
|
701
|
+
.cs-desk{background:#fff!important;padding:0!important;min-height:0!important}
|
|
702
|
+
.cs-sheet{box-shadow:none!important;margin:0 auto!important}
|
|
703
|
+
[data-omelette-chrome]{display:none!important}
|
|
704
|
+
</style></head><body>${clone.outerHTML}</body></html>`;
|
|
705
|
+
const url = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }));
|
|
706
|
+
let w = null;
|
|
707
|
+
try {
|
|
708
|
+
w = window.open(url, '_blank');
|
|
709
|
+
} catch (_) {
|
|
710
|
+
w = null;
|
|
711
|
+
}
|
|
712
|
+
if (!w) {
|
|
713
|
+
URL.revokeObjectURL(url);
|
|
714
|
+
window.print();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
let printed = false;
|
|
718
|
+
const go = () => {
|
|
719
|
+
if (printed) return;
|
|
720
|
+
printed = true;
|
|
721
|
+
try {
|
|
722
|
+
w.focus();
|
|
723
|
+
w.print();
|
|
724
|
+
} catch (_) {
|
|
725
|
+
/* ignore */
|
|
726
|
+
}
|
|
727
|
+
setTimeout(() => URL.revokeObjectURL(url), 4000);
|
|
728
|
+
};
|
|
729
|
+
w.addEventListener('load', () => setTimeout(go, 400));
|
|
730
|
+
setTimeout(go, 1600);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function downloadBlob(blob, filename) {
|
|
734
|
+
const type = blob.type || 'application/octet-stream';
|
|
735
|
+
try {
|
|
736
|
+
const file = new File([blob], filename, { type });
|
|
737
|
+
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
|
738
|
+
try {
|
|
739
|
+
await navigator.share({ files: [file], title: filename });
|
|
740
|
+
return;
|
|
741
|
+
} catch (e) {
|
|
742
|
+
if (e && e.name === 'AbortError') return;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} catch (_) {
|
|
746
|
+
/* File/share unsupported */
|
|
747
|
+
}
|
|
748
|
+
const url = URL.createObjectURL(blob);
|
|
349
749
|
const a = document.createElement('a');
|
|
350
|
-
a.href =
|
|
750
|
+
a.href = url;
|
|
351
751
|
a.download = filename;
|
|
352
752
|
a.rel = 'noopener';
|
|
753
|
+
a.target = '_blank';
|
|
353
754
|
document.body.appendChild(a);
|
|
354
755
|
a.click();
|
|
355
756
|
setTimeout(() => {
|
|
356
|
-
URL.revokeObjectURL(
|
|
757
|
+
URL.revokeObjectURL(url);
|
|
357
758
|
a.remove();
|
|
358
|
-
},
|
|
759
|
+
}, 4000);
|
|
359
760
|
}
|
|
360
761
|
|
|
361
762
|
function suggestName(ext) {
|
|
@@ -363,6 +764,24 @@
|
|
|
363
764
|
return t.replace(/[^\w\u00C0-\u024F\- ]+/g, '').replace(/\s+/g, '-').slice(0, 80) + ext;
|
|
364
765
|
}
|
|
365
766
|
|
|
767
|
+
async function downloadPdf() {
|
|
768
|
+
const sheet = document.querySelector('.cs-sheet') || document.body;
|
|
769
|
+
try {
|
|
770
|
+
const blob = await buildPdfBlob(sheet);
|
|
771
|
+
if (!blob || blob.size < 200) throw new Error('empty-pdf');
|
|
772
|
+
await downloadBlob(blob, suggestName('.pdf'));
|
|
773
|
+
} catch (e) {
|
|
774
|
+
console.warn('[doc-export] raster PDF failed, printing sheet clone', e);
|
|
775
|
+
printPdf();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function downloadDocx() {
|
|
780
|
+
const sheet = document.querySelector('.cs-sheet') || document.body;
|
|
781
|
+
const blob = await buildDocxBlob(sheet);
|
|
782
|
+
await downloadBlob(blob, suggestName('.docx'));
|
|
783
|
+
}
|
|
784
|
+
|
|
366
785
|
function injectToolbar() {
|
|
367
786
|
if (document.querySelector('[data-omelette-chrome="export"]')) return;
|
|
368
787
|
const meta = document.querySelector('meta[name="omelette-owns-print"]');
|
|
@@ -372,7 +791,7 @@
|
|
|
372
791
|
style.setAttribute('data-omelette-chrome', 'export-style');
|
|
373
792
|
style.textContent = `
|
|
374
793
|
[data-omelette-chrome="export"]{
|
|
375
|
-
position:fixed;z-index:2147483000;top:12px;right:12px;display:flex;gap:
|
|
794
|
+
position:fixed;z-index:2147483000;top:12px;right:12px;display:flex;flex-direction:column;align-items:flex-end;gap:4px;
|
|
376
795
|
font-family:var(--cs-font-family-ui,system-ui,sans-serif);pointer-events:auto;
|
|
377
796
|
}
|
|
378
797
|
[data-omelette-chrome="export"] button{
|
|
@@ -382,46 +801,105 @@
|
|
|
382
801
|
}
|
|
383
802
|
[data-omelette-chrome="export"] button:hover{background:#f7f0e6}
|
|
384
803
|
[data-omelette-chrome="export"] button:focus-visible{outline:2px solid #F4BA17;outline-offset:2px}
|
|
804
|
+
[data-cs-export="menu"]{
|
|
805
|
+
min-width:148px;background:#fff;border:1px solid #c9b8a8;border-radius:8px;
|
|
806
|
+
box-shadow:0 8px 24px rgba(35,26,18,.12);overflow:hidden;padding:4px;
|
|
807
|
+
}
|
|
808
|
+
[data-cs-export="menu"][hidden]{display:none!important}
|
|
809
|
+
[data-cs-export="menu"] button{
|
|
810
|
+
display:block;width:100%;border:0;border-radius:6px;box-shadow:none;text-align:left;font-weight:650;
|
|
811
|
+
}
|
|
385
812
|
@media print{
|
|
386
813
|
[data-omelette-chrome="export"],[data-omelette-chrome="export-style"]{display:none!important}
|
|
814
|
+
html,body,#dc-root,#dc-root>.sc-host{
|
|
815
|
+
height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important
|
|
816
|
+
}
|
|
387
817
|
}
|
|
388
818
|
`;
|
|
389
819
|
document.head.appendChild(style);
|
|
390
820
|
|
|
391
821
|
const bar = document.createElement('div');
|
|
392
822
|
bar.setAttribute('data-omelette-chrome', 'export');
|
|
393
|
-
bar.setAttribute('role', '
|
|
394
|
-
bar.setAttribute('aria-label', '
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
823
|
+
bar.setAttribute('role', 'group');
|
|
824
|
+
bar.setAttribute('aria-label', 'Tải tài liệu · Download document');
|
|
825
|
+
|
|
826
|
+
const trigger = document.createElement('button');
|
|
827
|
+
trigger.type = 'button';
|
|
828
|
+
trigger.setAttribute('data-cs-export', 'trigger');
|
|
829
|
+
trigger.setAttribute('aria-haspopup', 'menu');
|
|
830
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
831
|
+
trigger.textContent = 'Tải / Download ▾';
|
|
832
|
+
|
|
833
|
+
const menu = document.createElement('div');
|
|
834
|
+
menu.setAttribute('data-cs-export', 'menu');
|
|
835
|
+
menu.setAttribute('role', 'menu');
|
|
836
|
+
menu.hidden = true;
|
|
837
|
+
|
|
838
|
+
const pdfItem = document.createElement('button');
|
|
839
|
+
pdfItem.type = 'button';
|
|
840
|
+
pdfItem.setAttribute('role', 'menuitem');
|
|
841
|
+
pdfItem.setAttribute('data-cs-export', 'pdf');
|
|
842
|
+
pdfItem.textContent = 'PDF';
|
|
843
|
+
|
|
844
|
+
const docxItem = document.createElement('button');
|
|
845
|
+
docxItem.type = 'button';
|
|
846
|
+
docxItem.setAttribute('role', 'menuitem');
|
|
847
|
+
docxItem.setAttribute('data-cs-export', 'docx');
|
|
848
|
+
docxItem.textContent = 'DOCX';
|
|
849
|
+
|
|
850
|
+
const setOpen = (open) => {
|
|
851
|
+
menu.hidden = !open;
|
|
852
|
+
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
trigger.addEventListener('click', () => setOpen(menu.hidden));
|
|
856
|
+
trigger.addEventListener('keydown', (e) => {
|
|
857
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
858
|
+
e.preventDefault();
|
|
859
|
+
setOpen(true);
|
|
860
|
+
pdfItem.focus();
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
menu.addEventListener('keydown', (e) => {
|
|
864
|
+
if (e.key === 'Escape') {
|
|
865
|
+
e.preventDefault();
|
|
866
|
+
setOpen(false);
|
|
867
|
+
trigger.focus();
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
document.addEventListener('click', (e) => {
|
|
871
|
+
if (!bar.contains(e.target)) setOpen(false);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
pdfItem.addEventListener('click', async () => {
|
|
875
|
+
setOpen(false);
|
|
876
|
+
await downloadPdf();
|
|
877
|
+
});
|
|
878
|
+
docxItem.addEventListener('click', async () => {
|
|
879
|
+
setOpen(false);
|
|
405
880
|
try {
|
|
406
|
-
|
|
407
|
-
const blob = await buildDocxBlob(sheet);
|
|
408
|
-
downloadBlob(blob, suggestName('.docx'));
|
|
881
|
+
await downloadDocx();
|
|
409
882
|
} catch (e) {
|
|
410
883
|
console.error('[doc-export] DOCX failed', e);
|
|
411
884
|
alert('DOCX export failed: ' + (e && e.message ? e.message : e));
|
|
412
885
|
}
|
|
413
886
|
});
|
|
414
887
|
|
|
415
|
-
|
|
416
|
-
|
|
888
|
+
menu.appendChild(pdfItem);
|
|
889
|
+
menu.appendChild(docxItem);
|
|
890
|
+
bar.appendChild(trigger);
|
|
891
|
+
bar.appendChild(menu);
|
|
417
892
|
document.body.appendChild(bar);
|
|
418
893
|
}
|
|
419
894
|
|
|
420
|
-
// Test / automation hooks
|
|
421
895
|
window.__csDocExport = {
|
|
422
896
|
buildDocxBlob,
|
|
897
|
+
buildPdfBlob,
|
|
423
898
|
pageSizeFromCss,
|
|
424
899
|
injectToolbar,
|
|
900
|
+
printPdf,
|
|
901
|
+
downloadPdf,
|
|
902
|
+
downloadDocx,
|
|
425
903
|
};
|
|
426
904
|
|
|
427
905
|
const boot = () => injectToolbar();
|
|
@@ -430,6 +908,5 @@
|
|
|
430
908
|
} else {
|
|
431
909
|
boot();
|
|
432
910
|
}
|
|
433
|
-
// DC boots async — re-try after paint so .cs-sheet exists for future exports.
|
|
434
911
|
setTimeout(boot, 800);
|
|
435
912
|
})();
|