@slickgrid-universal/pdf-export 10.0.0 → 10.1.1
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/README.md +13 -0
- package/dist/pdfExport.service.d.ts +8 -0
- package/dist/pdfExport.service.d.ts.map +1 -1
- package/dist/pdfExport.service.js +119 -19
- package/dist/pdfExport.service.js.map +1 -1
- package/package.json +4 -4
- package/src/pdfExport.service.spec.ts +827 -5
- package/src/pdfExport.service.ts +136 -22
package/src/pdfExport.service.ts
CHANGED
|
@@ -33,6 +33,12 @@ const DEFAULT_EXPORT_OPTIONS: PdfExportOption = {
|
|
|
33
33
|
dataRowBackgroundOffset: -1,
|
|
34
34
|
headerTextOffset: -10,
|
|
35
35
|
headerBackgroundOffset: -5,
|
|
36
|
+
headerBackgroundColor: [66, 139, 202],
|
|
37
|
+
headerTextColor: [255, 255, 255],
|
|
38
|
+
preHeaderBackgroundColor: [108, 117, 125],
|
|
39
|
+
preHeaderTextColor: [255, 255, 255],
|
|
40
|
+
alternateRowColor: [245, 245, 245],
|
|
41
|
+
cellPadding: 4,
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
export interface GroupedHeaderSpan {
|
|
@@ -158,7 +164,7 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
158
164
|
|
|
159
165
|
// cache resolved export options for each column as a Record by column.id
|
|
160
166
|
const columnExportOptionsCache: Record<string, PdfExportOption> = {};
|
|
161
|
-
columns.forEach((col) => {
|
|
167
|
+
columns.forEach((col: Column) => {
|
|
162
168
|
if (col.id) {
|
|
163
169
|
columnExportOptionsCache[col.id] = resolveColumnExportOptions(col, this._exportOptions);
|
|
164
170
|
}
|
|
@@ -174,6 +180,11 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
174
180
|
format: this._exportOptions.pageSize || 'a4',
|
|
175
181
|
});
|
|
176
182
|
|
|
183
|
+
// Set PDF document properties (metadata) if provided
|
|
184
|
+
if (this._exportOptions.documentProperties) {
|
|
185
|
+
doc.setDocumentProperties(this._exportOptions.documentProperties);
|
|
186
|
+
}
|
|
187
|
+
|
|
177
188
|
let startY = 40;
|
|
178
189
|
if (this._exportOptions.documentTitle) {
|
|
179
190
|
doc.setFontSize(16);
|
|
@@ -193,30 +204,71 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
193
204
|
|
|
194
205
|
// Add table (using jsPDF-AutoTable if available, else fallback to manual)
|
|
195
206
|
if ((doc as any).autoTable) {
|
|
196
|
-
//
|
|
197
|
-
(
|
|
198
|
-
|
|
207
|
+
// Build per-column styles for AutoTable from each column's pdfExportOptions
|
|
208
|
+
const visibleColumns = columns.filter((col: Column) => !col.excludeFromExport && (col.width === undefined || col.width > 0));
|
|
209
|
+
const columnStyles: Record<number, { halign: string }> = {};
|
|
210
|
+
const groupOffset = this._hasGroupedItems ? 1 : 0;
|
|
211
|
+
for (const [idx, col] of visibleColumns.entries()) {
|
|
212
|
+
const colExportOpts = resolveColumnExportOptions(col, this._exportOptions);
|
|
213
|
+
if (colExportOpts.textAlign && colExportOpts.textAlign !== (this._exportOptions.textAlign || 'left')) {
|
|
214
|
+
columnStyles[idx + groupOffset] = { halign: colExportOpts.textAlign };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Build per-column header alignment map for didParseCell hook
|
|
219
|
+
const headerAlignMap: Record<number, string> = {};
|
|
220
|
+
for (const [idx, col] of visibleColumns.entries()) {
|
|
221
|
+
const colExportOpts = resolveColumnExportOptions(col, this._exportOptions);
|
|
222
|
+
if (colExportOpts.textAlign && colExportOpts.textAlign !== (this._exportOptions.textAlign || 'left')) {
|
|
223
|
+
headerAlignMap[idx + groupOffset] = colExportOpts.textAlign;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let autoTableOpts: Record<string, unknown> = {
|
|
228
|
+
head: this._buildAutoTableHead(headers, hasColumnTitlePreHeader, groupByColumnHeader),
|
|
199
229
|
body: data,
|
|
200
230
|
startY,
|
|
231
|
+
columnStyles,
|
|
201
232
|
styles: {
|
|
202
|
-
fontSize: this._exportOptions.fontSize
|
|
203
|
-
cellPadding:
|
|
233
|
+
fontSize: this._exportOptions.fontSize!,
|
|
234
|
+
cellPadding: this._exportOptions.cellPadding!,
|
|
204
235
|
overflow: 'linebreak',
|
|
205
236
|
halign: this._exportOptions.textAlign || 'left',
|
|
206
237
|
},
|
|
207
238
|
headStyles: {
|
|
208
|
-
fontSize: this._exportOptions.headerFontSize
|
|
209
|
-
fillColor:
|
|
210
|
-
textColor:
|
|
239
|
+
fontSize: this._exportOptions.headerFontSize!,
|
|
240
|
+
fillColor: this._exportOptions.headerBackgroundColor!,
|
|
241
|
+
textColor: this._exportOptions.headerTextColor!,
|
|
211
242
|
halign: this._exportOptions.textAlign || 'left',
|
|
212
243
|
valign: 'middle',
|
|
213
244
|
},
|
|
245
|
+
// Align header cells to match their column's textAlign and style pre-header row
|
|
246
|
+
didParseCell: (data: any) => {
|
|
247
|
+
// Style pre-header row with distinct colors
|
|
248
|
+
if (data.section === 'head' && hasColumnTitlePreHeader && data.row.index === 0) {
|
|
249
|
+
data.cell.styles.fillColor = this._exportOptions.preHeaderBackgroundColor!;
|
|
250
|
+
data.cell.styles.textColor = this._exportOptions.preHeaderTextColor!;
|
|
251
|
+
data.cell.styles.halign = 'center';
|
|
252
|
+
}
|
|
253
|
+
// Align column header cells (last head row) to match their column's textAlign
|
|
254
|
+
const isColumnHeaderRow = hasColumnTitlePreHeader ? data.row.index === 1 : data.row.index === 0;
|
|
255
|
+
if (data.section === 'head' && isColumnHeaderRow && headerAlignMap[data.column.index]) {
|
|
256
|
+
data.cell.styles.halign = headerAlignMap[data.column.index];
|
|
257
|
+
}
|
|
258
|
+
},
|
|
214
259
|
alternateRowStyles: {
|
|
215
|
-
fillColor:
|
|
260
|
+
fillColor: this._exportOptions.alternateRowColor!,
|
|
216
261
|
},
|
|
217
262
|
margin: { left: 40, right: 40 },
|
|
218
263
|
theme: 'grid',
|
|
219
|
-
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Allow users to customize AutoTable options via callback
|
|
267
|
+
if (typeof this._exportOptions.autoTableOptions === 'function') {
|
|
268
|
+
autoTableOpts = this._exportOptions.autoTableOptions(autoTableOpts);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
(doc as any).autoTable(autoTableOpts);
|
|
220
272
|
} else {
|
|
221
273
|
// Fallback: manual table rendering (no cell borders)
|
|
222
274
|
// Use cached columnExportOptionsCache for per-column options
|
|
@@ -277,8 +329,22 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
277
329
|
y = this._drawPreHeaderRow(doc, y, colWidths, margin, headerTextOffset, headerBackgroundOffset, groupByColumnHeader);
|
|
278
330
|
}
|
|
279
331
|
|
|
332
|
+
// Build per-column header alignment array for manual header drawing
|
|
333
|
+
const headerAligns: Array<'left' | 'center' | 'right'> = headers.map((_, idx) => {
|
|
334
|
+
if (this._hasGroupedItems && idx === 0) {
|
|
335
|
+
return this._exportOptions.textAlign || 'left';
|
|
336
|
+
}
|
|
337
|
+
const dataColIdx = this._hasGroupedItems ? idx - 1 : idx;
|
|
338
|
+
const colDef = columns.filter((col: Column) => !col.excludeFromExport)[dataColIdx];
|
|
339
|
+
if (colDef?.id) {
|
|
340
|
+
const colOpt = columnExportOptionsCache[colDef.id];
|
|
341
|
+
return colOpt?.textAlign || this._exportOptions.textAlign || 'left';
|
|
342
|
+
}
|
|
343
|
+
return this._exportOptions.textAlign || 'left';
|
|
344
|
+
});
|
|
345
|
+
|
|
280
346
|
// Draw header row
|
|
281
|
-
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset);
|
|
347
|
+
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset, headerAligns);
|
|
282
348
|
doc.setFontSize(this._exportOptions.fontSize || 10);
|
|
283
349
|
data.forEach((row, rowIdx) => {
|
|
284
350
|
// Check for page break before drawing row
|
|
@@ -292,18 +358,19 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
292
358
|
y = this._drawPreHeaderRow(doc, y, colWidths, margin, headerTextOffset, headerBackgroundOffset, groupByColumnHeader);
|
|
293
359
|
}
|
|
294
360
|
// Header row
|
|
295
|
-
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset);
|
|
361
|
+
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset, headerAligns);
|
|
296
362
|
doc.setFontSize(this._exportOptions.fontSize || 10);
|
|
297
363
|
}
|
|
298
364
|
}
|
|
299
365
|
// Alternate row background
|
|
300
366
|
if (rowIdx % 2 === 1) {
|
|
301
|
-
|
|
367
|
+
const altColor = this._exportOptions.alternateRowColor!;
|
|
368
|
+
doc.setFillColor(altColor[0], altColor[1], altColor[2]);
|
|
302
369
|
// Use first column's dataRowBackgroundOffset for the row
|
|
303
370
|
// Always use the correct column for background offset (first visible data column)
|
|
304
371
|
let firstDataColIdx = 0;
|
|
305
372
|
if (this._hasGroupedItems) firstDataColIdx = 1;
|
|
306
|
-
const firstColId = columns.filter((col) => !col.excludeFromExport)[firstDataColIdx]?.id;
|
|
373
|
+
const firstColId = columns.filter((col: Column) => !col.excludeFromExport)[firstDataColIdx]?.id;
|
|
307
374
|
const colOpt = firstColId ? columnExportOptionsCache[firstColId] : this._exportOptions;
|
|
308
375
|
doc.rect(
|
|
309
376
|
margin,
|
|
@@ -334,7 +401,7 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
334
401
|
// Group column: no colDef, use default options
|
|
335
402
|
} else {
|
|
336
403
|
const dataColIdx = this._hasGroupedItems ? colIdx - 1 : colIdx;
|
|
337
|
-
colDef = columns.filter((col) => !col.excludeFromExport)[dataColIdx];
|
|
404
|
+
colDef = columns.filter((col: Column) => !col.excludeFromExport)[dataColIdx];
|
|
338
405
|
if (colDef && colDef.id) {
|
|
339
406
|
colOpt = columnExportOptionsCache[colDef.id] || this._exportOptions;
|
|
340
407
|
}
|
|
@@ -633,6 +700,41 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
633
700
|
return outputRow;
|
|
634
701
|
}
|
|
635
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Build the `head` array for jsPDF-AutoTable.
|
|
705
|
+
* When grouped column headers (pre-header) are present, returns two rows:
|
|
706
|
+
* [preHeaderRow, headerRow]
|
|
707
|
+
* where pre-header cells use `{ content, colSpan }` to span multiple columns.
|
|
708
|
+
* Otherwise returns a single row: [headerRow]
|
|
709
|
+
*/
|
|
710
|
+
private _buildAutoTableHead(
|
|
711
|
+
headers: string[],
|
|
712
|
+
hasPreHeader: boolean,
|
|
713
|
+
groupByColumnHeader?: string
|
|
714
|
+
): Array<Array<string | { content: string; colSpan: number }>> {
|
|
715
|
+
if (!hasPreHeader || !this._groupedColumnHeaders || this._groupedColumnHeaders.length === 0) {
|
|
716
|
+
return [headers];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Build the pre-header row with colSpan cells
|
|
720
|
+
const preHeaderRow: Array<string | { content: string; colSpan: number }> = [];
|
|
721
|
+
|
|
722
|
+
// If grid has grouping (drag & drop), the first column is the group-by column
|
|
723
|
+
if (this._hasGroupedItems && groupByColumnHeader) {
|
|
724
|
+
preHeaderRow.push('');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (const group of this._groupedColumnHeaders) {
|
|
728
|
+
if (group.span > 1) {
|
|
729
|
+
preHeaderRow.push({ content: group.title, colSpan: group.span });
|
|
730
|
+
} else {
|
|
731
|
+
preHeaderRow.push(group.title);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return [preHeaderRow, headers];
|
|
736
|
+
}
|
|
737
|
+
|
|
636
738
|
/**
|
|
637
739
|
* Get all Grouped Header Titles and their keys, translate the title when required.
|
|
638
740
|
* Returns array of { title, span } for each group, in order
|
|
@@ -683,8 +785,10 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
683
785
|
groupByColumnHeader?: string
|
|
684
786
|
): number {
|
|
685
787
|
doc.setFontSize(this._exportOptions.headerFontSize || 11);
|
|
686
|
-
|
|
687
|
-
doc.
|
|
788
|
+
const preHdrBg = this._exportOptions.preHeaderBackgroundColor ?? [108, 117, 125];
|
|
789
|
+
doc.setFillColor(preHdrBg[0], preHdrBg[1], preHdrBg[2]);
|
|
790
|
+
const preHdrTxt = this._exportOptions.preHeaderTextColor ?? [255, 255, 255];
|
|
791
|
+
doc.setTextColor(preHdrTxt[0], preHdrTxt[1], preHdrTxt[2]);
|
|
688
792
|
// colCount is not needed
|
|
689
793
|
const preHeaderWidth = colWidths.reduce((a, b) => a + b, 0);
|
|
690
794
|
doc.rect(margin, y - 14 + headerBackgroundOffset, preHeaderWidth, 20, 'F');
|
|
@@ -721,16 +825,26 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
721
825
|
colWidths: number[],
|
|
722
826
|
margin: number,
|
|
723
827
|
headerTextOffset: number,
|
|
724
|
-
headerBackgroundOffset: number
|
|
828
|
+
headerBackgroundOffset: number,
|
|
829
|
+
headerAligns?: Array<'left' | 'center' | 'right'>
|
|
725
830
|
): number {
|
|
726
831
|
doc.setFontSize(this._exportOptions.headerFontSize || 11);
|
|
727
|
-
|
|
728
|
-
doc.
|
|
832
|
+
const hdrBg = this._exportOptions.headerBackgroundColor ?? [66, 139, 202];
|
|
833
|
+
doc.setFillColor(hdrBg[0], hdrBg[1], hdrBg[2]);
|
|
834
|
+
const hdrTxt = this._exportOptions.headerTextColor ?? [255, 255, 255];
|
|
835
|
+
doc.setTextColor(hdrTxt[0], hdrTxt[1], hdrTxt[2]);
|
|
729
836
|
const headerWidth = colWidths.reduce((a, b) => a + b, 0);
|
|
730
837
|
doc.rect(margin, y - 14 + headerBackgroundOffset, headerWidth, 20, 'F');
|
|
731
838
|
let headerX = margin;
|
|
732
839
|
headers.forEach((header, idx) => {
|
|
733
|
-
|
|
840
|
+
const align = headerAligns?.[idx] || 'left';
|
|
841
|
+
let textX = headerX;
|
|
842
|
+
if (align === 'center') {
|
|
843
|
+
textX = headerX + colWidths[idx] / 2;
|
|
844
|
+
} else if (align === 'right') {
|
|
845
|
+
textX = headerX + colWidths[idx];
|
|
846
|
+
}
|
|
847
|
+
doc.text(String(header), textX, y + headerTextOffset, { align, baseline: 'middle' });
|
|
734
848
|
headerX += colWidths[idx];
|
|
735
849
|
});
|
|
736
850
|
return y + 20;
|