@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.
@@ -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
- setTimeout(() => {
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
- // For jsPDF-AutoTable, only global options are supported (no per-column)
196
- (doc as any).autoTable({
197
- head: [headers],
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 || 10,
202
- cellPadding: 4,
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 || 11,
208
- fillColor: [66, 139, 202], // blue header
209
- textColor: 255,
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: [245, 245, 245], // light gray for odd rows
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
- doc.setFillColor(245, 245, 245);
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
- doc.setFillColor(108, 117, 125); // #6c757d
686
- doc.setTextColor(255, 255, 255);
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
- doc.setFillColor(66, 139, 202);
727
- doc.setTextColor(255, 255, 255);
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
- doc.text(String(header), headerX, y + headerTextOffset, { align: 'left', baseline: 'middle' });
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;