@slickgrid-universal/pdf-export 10.0.0-beta.0 → 10.1.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/README.md +13 -1
- package/dist/pdfExport.service.d.ts +9 -1
- package/dist/pdfExport.service.d.ts.map +1 -1
- package/dist/pdfExport.service.js +122 -20
- package/dist/pdfExport.service.js.map +1 -1
- package/package.json +5 -5
- package/src/pdfExport.service.spec.ts +827 -5
- package/src/pdfExport.service.ts +140 -25
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 {
|
|
@@ -62,12 +68,11 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
62
68
|
protected _locales!: Locale;
|
|
63
69
|
protected _pubSubService!: PubSubService | null;
|
|
64
70
|
protected _translaterService: TranslaterService | undefined;
|
|
71
|
+
protected _timer?: any;
|
|
65
72
|
|
|
66
73
|
/** PdfExportService class name which is use to find service instance in the external registered services */
|
|
67
74
|
readonly pluginName = 'PdfExportService';
|
|
68
75
|
|
|
69
|
-
constructor() {}
|
|
70
|
-
|
|
71
76
|
protected get _datasetIdPropName(): string {
|
|
72
77
|
return (this._gridOptions && this._gridOptions.datasetIdPropertyName) || 'id';
|
|
73
78
|
}
|
|
@@ -83,6 +88,7 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
dispose(): void {
|
|
91
|
+
clearTimeout(this._timer);
|
|
86
92
|
this._pubSubService?.unsubscribeAll();
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -124,7 +130,8 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
124
130
|
this._exportOptions = extend(true, {}, { ...DEFAULT_EXPORT_OPTIONS, ...this._gridOptions.pdfExportOptions, ...options });
|
|
125
131
|
|
|
126
132
|
// wrap it into a setTimeout so that the EventAggregator has enough time to start a pre-process like showing a spinner
|
|
127
|
-
|
|
133
|
+
clearTimeout(this._timer);
|
|
134
|
+
this._timer = setTimeout(() => {
|
|
128
135
|
try {
|
|
129
136
|
const columns = this._grid.getColumns() || [];
|
|
130
137
|
|
|
@@ -157,7 +164,7 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
157
164
|
|
|
158
165
|
// cache resolved export options for each column as a Record by column.id
|
|
159
166
|
const columnExportOptionsCache: Record<string, PdfExportOption> = {};
|
|
160
|
-
columns.forEach((col) => {
|
|
167
|
+
columns.forEach((col: Column) => {
|
|
161
168
|
if (col.id) {
|
|
162
169
|
columnExportOptionsCache[col.id] = resolveColumnExportOptions(col, this._exportOptions);
|
|
163
170
|
}
|
|
@@ -173,6 +180,11 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
173
180
|
format: this._exportOptions.pageSize || 'a4',
|
|
174
181
|
});
|
|
175
182
|
|
|
183
|
+
// Set PDF document properties (metadata) if provided
|
|
184
|
+
if (this._exportOptions.documentProperties) {
|
|
185
|
+
doc.setDocumentProperties(this._exportOptions.documentProperties);
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
let startY = 40;
|
|
177
189
|
if (this._exportOptions.documentTitle) {
|
|
178
190
|
doc.setFontSize(16);
|
|
@@ -192,30 +204,71 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
192
204
|
|
|
193
205
|
// Add table (using jsPDF-AutoTable if available, else fallback to manual)
|
|
194
206
|
if ((doc as any).autoTable) {
|
|
195
|
-
//
|
|
196
|
-
(
|
|
197
|
-
|
|
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),
|
|
198
229
|
body: data,
|
|
199
230
|
startY,
|
|
231
|
+
columnStyles,
|
|
200
232
|
styles: {
|
|
201
|
-
fontSize: this._exportOptions.fontSize
|
|
202
|
-
cellPadding:
|
|
233
|
+
fontSize: this._exportOptions.fontSize!,
|
|
234
|
+
cellPadding: this._exportOptions.cellPadding!,
|
|
203
235
|
overflow: 'linebreak',
|
|
204
236
|
halign: this._exportOptions.textAlign || 'left',
|
|
205
237
|
},
|
|
206
238
|
headStyles: {
|
|
207
|
-
fontSize: this._exportOptions.headerFontSize
|
|
208
|
-
fillColor:
|
|
209
|
-
textColor:
|
|
239
|
+
fontSize: this._exportOptions.headerFontSize!,
|
|
240
|
+
fillColor: this._exportOptions.headerBackgroundColor!,
|
|
241
|
+
textColor: this._exportOptions.headerTextColor!,
|
|
210
242
|
halign: this._exportOptions.textAlign || 'left',
|
|
211
243
|
valign: 'middle',
|
|
212
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
|
+
},
|
|
213
259
|
alternateRowStyles: {
|
|
214
|
-
fillColor:
|
|
260
|
+
fillColor: this._exportOptions.alternateRowColor!,
|
|
215
261
|
},
|
|
216
262
|
margin: { left: 40, right: 40 },
|
|
217
263
|
theme: 'grid',
|
|
218
|
-
}
|
|
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);
|
|
219
272
|
} else {
|
|
220
273
|
// Fallback: manual table rendering (no cell borders)
|
|
221
274
|
// Use cached columnExportOptionsCache for per-column options
|
|
@@ -276,8 +329,22 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
276
329
|
y = this._drawPreHeaderRow(doc, y, colWidths, margin, headerTextOffset, headerBackgroundOffset, groupByColumnHeader);
|
|
277
330
|
}
|
|
278
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
|
+
|
|
279
346
|
// Draw header row
|
|
280
|
-
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset);
|
|
347
|
+
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset, headerAligns);
|
|
281
348
|
doc.setFontSize(this._exportOptions.fontSize || 10);
|
|
282
349
|
data.forEach((row, rowIdx) => {
|
|
283
350
|
// Check for page break before drawing row
|
|
@@ -291,18 +358,19 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
291
358
|
y = this._drawPreHeaderRow(doc, y, colWidths, margin, headerTextOffset, headerBackgroundOffset, groupByColumnHeader);
|
|
292
359
|
}
|
|
293
360
|
// Header row
|
|
294
|
-
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset);
|
|
361
|
+
y = this._drawHeaderRow(doc, y, headers, colWidths, margin, headerTextOffset, headerBackgroundOffset, headerAligns);
|
|
295
362
|
doc.setFontSize(this._exportOptions.fontSize || 10);
|
|
296
363
|
}
|
|
297
364
|
}
|
|
298
365
|
// Alternate row background
|
|
299
366
|
if (rowIdx % 2 === 1) {
|
|
300
|
-
|
|
367
|
+
const altColor = this._exportOptions.alternateRowColor!;
|
|
368
|
+
doc.setFillColor(altColor[0], altColor[1], altColor[2]);
|
|
301
369
|
// Use first column's dataRowBackgroundOffset for the row
|
|
302
370
|
// Always use the correct column for background offset (first visible data column)
|
|
303
371
|
let firstDataColIdx = 0;
|
|
304
372
|
if (this._hasGroupedItems) firstDataColIdx = 1;
|
|
305
|
-
const firstColId = columns.filter((col) => !col.excludeFromExport)[firstDataColIdx]?.id;
|
|
373
|
+
const firstColId = columns.filter((col: Column) => !col.excludeFromExport)[firstDataColIdx]?.id;
|
|
306
374
|
const colOpt = firstColId ? columnExportOptionsCache[firstColId] : this._exportOptions;
|
|
307
375
|
doc.rect(
|
|
308
376
|
margin,
|
|
@@ -333,7 +401,7 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
333
401
|
// Group column: no colDef, use default options
|
|
334
402
|
} else {
|
|
335
403
|
const dataColIdx = this._hasGroupedItems ? colIdx - 1 : colIdx;
|
|
336
|
-
colDef = columns.filter((col) => !col.excludeFromExport)[dataColIdx];
|
|
404
|
+
colDef = columns.filter((col: Column) => !col.excludeFromExport)[dataColIdx];
|
|
337
405
|
if (colDef && colDef.id) {
|
|
338
406
|
colOpt = columnExportOptionsCache[colDef.id] || this._exportOptions;
|
|
339
407
|
}
|
|
@@ -632,6 +700,41 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
632
700
|
return outputRow;
|
|
633
701
|
}
|
|
634
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
|
+
|
|
635
738
|
/**
|
|
636
739
|
* Get all Grouped Header Titles and their keys, translate the title when required.
|
|
637
740
|
* Returns array of { title, span } for each group, in order
|
|
@@ -682,8 +785,10 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
682
785
|
groupByColumnHeader?: string
|
|
683
786
|
): number {
|
|
684
787
|
doc.setFontSize(this._exportOptions.headerFontSize || 11);
|
|
685
|
-
|
|
686
|
-
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]);
|
|
687
792
|
// colCount is not needed
|
|
688
793
|
const preHeaderWidth = colWidths.reduce((a, b) => a + b, 0);
|
|
689
794
|
doc.rect(margin, y - 14 + headerBackgroundOffset, preHeaderWidth, 20, 'F');
|
|
@@ -720,16 +825,26 @@ export class PdfExportService implements ExternalResource, BasePdfExportService
|
|
|
720
825
|
colWidths: number[],
|
|
721
826
|
margin: number,
|
|
722
827
|
headerTextOffset: number,
|
|
723
|
-
headerBackgroundOffset: number
|
|
828
|
+
headerBackgroundOffset: number,
|
|
829
|
+
headerAligns?: Array<'left' | 'center' | 'right'>
|
|
724
830
|
): number {
|
|
725
831
|
doc.setFontSize(this._exportOptions.headerFontSize || 11);
|
|
726
|
-
|
|
727
|
-
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]);
|
|
728
836
|
const headerWidth = colWidths.reduce((a, b) => a + b, 0);
|
|
729
837
|
doc.rect(margin, y - 14 + headerBackgroundOffset, headerWidth, 20, 'F');
|
|
730
838
|
let headerX = margin;
|
|
731
839
|
headers.forEach((header, idx) => {
|
|
732
|
-
|
|
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' });
|
|
733
848
|
headerX += colWidths[idx];
|
|
734
849
|
});
|
|
735
850
|
return y + 20;
|