Generate PDF tables with JavaScript (jsPDF plugin)
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Event-driven customization system for intercepting and modifying table rendering at various stages, enabling advanced customization and dynamic content generation.
Base classes providing context information for hook functions.
Base hook data class providing table and document context.
class HookData {
/** Reference to the table being rendered */
table: Table;
/** Current page number */
pageNumber: number;
/** Table settings */
settings: Settings;
/** jsPDF document instance */
doc: jsPDFDocument;
/** Current cursor position */
cursor: Pos | null;
constructor(doc: DocHandler, table: Table, cursor: Pos | null);
}Extended hook data class for cell-specific events.
class CellHookData extends HookData {
/** Reference to the current cell */
cell: Cell;
/** Reference to the current row */
row: Row;
/** Reference to the current column */
column: Column;
/** Section type: 'head', 'body', or 'foot' */
section: "head" | "body" | "foot";
constructor(
doc: DocHandler,
table: Table,
cell: Cell,
row: Row,
column: Column,
cursor: Pos | null
);
}Function signatures for different hook events.
/** Page-level hook function */
type PageHook = (data: HookData) => void | boolean;
/** Cell-level hook function */
type CellHook = (data: CellHookData) => void | boolean;Available hook events for customizing table rendering behavior.
Called after the plugin has finished parsing cell content. Use this to override content or styles for specific cells.
/** Called when the plugin finished parsing cell content */
didParseCell?: CellHook;Usage Example:
autoTable(doc, {
head: [['Name', 'Amount', 'Status']],
body: [
['John Doe', '1500', 'active'],
['Jane Smith', '2300', 'inactive'],
],
didParseCell: (data) => {
const { cell, column } = data;
// Format currency
if (column.dataKey === 1) { // Amount column
const amount = parseFloat(cell.text[0]);
cell.text = [`$${amount.toLocaleString()}`];
}
// Style status column
if (column.dataKey === 2) { // Status column
if (cell.text[0] === 'active') {
cell.styles.textColor = [0, 128, 0];
cell.text = ['✓ Active'];
} else {
cell.styles.textColor = [128, 0, 0];
cell.text = ['✗ Inactive'];
}
}
},
});Called before a cell is drawn. Use this to modify styling or positioning before rendering.
/** Called before a cell or row is drawn */
willDrawCell?: CellHook;Usage Example:
autoTable(doc, {
head: [['Product', 'Stock', 'Price']],
body: [
['Laptop', '5', '999'],
['Phone', '0', '599'],
['Tablet', '12', '399'],
],
willDrawCell: (data) => {
const { cell, column, doc } = data;
// Highlight low stock items
if (column.dataKey === 1) { // Stock column
const stock = parseInt(cell.text[0]);
if (stock === 0) {
cell.styles.fillColor = [255, 200, 200]; // Light red
cell.styles.fontStyle = 'bold';
} else if (stock < 10) {
cell.styles.fillColor = [255, 255, 200]; // Light yellow
}
}
// Apply custom jsPDF styling
if (column.dataKey === 2) { // Price column
doc.setTextColor(0, 100, 0); // Dark green for prices
}
},
});Called after a cell has been drawn. Use this to add additional content like images, shapes, or custom text.
/** Called after a cell has been added to the page */
didDrawCell?: CellHook;Usage Example:
autoTable(doc, {
head: [['Name', 'Rating', 'Notes']],
body: [
['Product A', '5', 'Excellent quality'],
['Product B', '3', 'Average'],
['Product C', '4', 'Good value'],
],
didDrawCell: (data) => {
const { cell, column, doc } = data;
// Draw star ratings
if (column.dataKey === 1) { // Rating column
const rating = parseInt(cell.text[0]);
const startX = cell.x + 5;
const centerY = cell.y + cell.height / 2;
// Draw stars
for (let i = 0; i < 5; i++) {
const x = startX + (i * 10);
if (i < rating) {
doc.setFillColor(255, 215, 0); // Gold
doc.circle(x, centerY, 3, 'F');
} else {
doc.setDrawColor(200, 200, 200);
doc.circle(x, centerY, 3, 'S');
}
}
}
// Add custom borders
if (column.dataKey === 2) { // Notes column
doc.setDrawColor(100, 100, 100);
doc.setLineWidth(0.5);
doc.line(cell.x, cell.y, cell.x + cell.width, cell.y);
}
},
});Called before starting to draw content on a page. Use this to add headers, watermarks, or page setup.
/** Called before starting to draw on a page */
willDrawPage?: PageHook;Usage Example:
autoTable(doc, {
head: [['Item', 'Quantity', 'Price']],
body: largeDataSet,
willDrawPage: (data) => {
const { doc, table, pageNumber } = data;
// Add header to each page
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Sales Report', 20, 20);
// Add page number
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text(`Page ${pageNumber}`, 200, 20);
// Add watermark
doc.setTextColor(200, 200, 200);
doc.setFontSize(48);
doc.text('DRAFT', 105, 150, {
angle: 45,
align: 'center'
});
// Reset text color for table content
doc.setTextColor(0, 0, 0);
doc.setFontSize(10);
},
});Called after the plugin has finished drawing everything on a page. Use this to add footers, signatures, or final page elements.
/** Called after the plugin has finished drawing everything on a page */
didDrawPage?: PageHook;Usage Example:
autoTable(doc, {
head: [['Description', 'Amount']],
body: invoiceItems,
didDrawPage: (data) => {
const { doc, table, pageNumber, cursor } = data;
// Add footer
const pageHeight = doc.internal.pageSize.height;
const pageWidth = doc.internal.pageSize.width;
doc.setFontSize(8);
doc.setTextColor(100, 100, 100);
// Company information
doc.text('MyCompany Inc.', 20, pageHeight - 20);
doc.text('123 Business St, City, State 12345', 20, pageHeight - 15);
doc.text('Phone: (555) 123-4567', 20, pageHeight - 10);
// Page number and date
doc.text(`Page ${pageNumber}`, pageWidth - 50, pageHeight - 20);
doc.text(new Date().toLocaleDateString(), pageWidth - 50, pageHeight - 15);
// Add total on last page
if (cursor && cursor.y > pageHeight - 100) {
const totalY = cursor.y + 10;
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.text('Total: $1,234.56', pageWidth - 80, totalY);
}
},
});Collection of all hook functions for a table.
interface HookProps {
didParseCell: CellHook[];
willDrawCell: CellHook[];
didDrawCell: CellHook[];
willDrawPage: PageHook[];
didDrawPage: PageHook[];
}const didParseCell = (data: CellHookData) => {
const { cell, column, row, section } = data;
// Only modify body cells
if (section !== 'body') return;
// Format dates in specific column
if (column.dataKey === 'date') {
const date = new Date(cell.text[0]);
cell.text = [date.toLocaleDateString()];
}
// Add row numbers
if (column.index === 0) {
cell.text = [`${row.index + 1}. ${cell.text[0]}`];
}
};const willDrawCell = (data: CellHookData) => {
const { cell, row, table } = data;
// Alternate row colors differently for each section
switch (data.section) {
case 'head':
cell.styles.fillColor = [52, 73, 94];
cell.styles.textColor = [255, 255, 255];
break;
case 'body':
if (row.index % 2 === 0) {
cell.styles.fillColor = [245, 245, 245];
}
break;
case 'foot':
cell.styles.fillColor = [52, 152, 219];
cell.styles.textColor = [255, 255, 255];
cell.styles.fontStyle = 'bold';
break;
}
};let pageCount = 0;
const willDrawPage = (data: HookData) => {
pageCount++;
const { doc } = data;
// Different header for first page
if (pageCount === 1) {
doc.setFontSize(20);
doc.text('Annual Report 2024', 20, 30);
doc.setFontSize(12);
doc.text('Confidential Document', 20, 40);
} else {
doc.setFontSize(12);
doc.text('Annual Report 2024 (Continued)', 20, 20);
}
};
const didDrawPage = (data: HookData) => {
const { doc, pageNumber } = data;
const pageHeight = doc.internal.pageSize.height;
// Page footer with different content for last page
if (pageNumber === pageCount) {
doc.text('End of Report', 20, pageHeight - 10);
} else {
doc.text('Continued on next page...', 20, pageHeight - 10);
}
};Hooks can return false to prevent default behavior:
const willDrawCell = (data: CellHookData) => {
const { cell, column } = data;
// Skip drawing empty cells
if (!cell.text[0] || cell.text[0].trim() === '') {
return false; // Prevents cell from being drawn
}
// Skip drawing specific columns
if (column.dataKey === 'internal_id') {
return false;
}
};type Pos = { x: number; y: number };Hook functions receive cursor position information for precise element placement:
const didDrawCell = (data: CellHookData) => {
const { cell, doc, cursor } = data;
console.log(`Cell position: (${cell.x}, ${cell.y})`);
console.log(`Cell size: ${cell.width} x ${cell.height}`);
console.log(`Cursor position: (${cursor?.x}, ${cursor?.y})`);
// Use position for custom drawing
doc.circle(cell.x + cell.width - 5, cell.y + 5, 2, 'F');
};