@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.
@@ -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
- // For jsPDF-AutoTable, only global options are supported (no per-column)
197
- (doc as any).autoTable({
198
- 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),
199
229
  body: data,
200
230
  startY,
231
+ columnStyles,
201
232
  styles: {
202
- fontSize: this._exportOptions.fontSize || 10,
203
- cellPadding: 4,
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 || 11,
209
- fillColor: [66, 139, 202], // blue header
210
- textColor: 255,
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: [245, 245, 245], // light gray for odd rows
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
- doc.setFillColor(245, 245, 245);
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
- doc.setFillColor(108, 117, 125); // #6c757d
687
- 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]);
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
- doc.setFillColor(66, 139, 202);
728
- 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]);
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
- 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' });
734
848
  headerX += colWidths[idx];
735
849
  });
736
850
  return y + 20;