Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Align text in a table column vertically #74

Open
jimmyting44 opened this issue Aug 19, 2014 · 138 comments
Open

Align text in a table column vertically #74

jimmyting44 opened this issue Aug 19, 2014 · 138 comments

Comments

@jimmyting44
Copy link

Is it possible to center some text vertically such that the text is the same distance apart from the top and bottom borders?

I'm aware of alignment : 'center' but this only works horizontally.

@bpampuch
Copy link
Owner

not at the moment, this would be a little bit problematic to support all boundary cases

@TheShakyCoder
Copy link

Yes. to be able to align text to the bottom border of a table cell would be useful.

@silveur
Copy link

silveur commented Oct 31, 2015

+1

1 similar comment
@tihomirsmudj
Copy link

+1

@serkandurusoy
Copy link

I've managed to do this by using html canvas to create vertical text and then importing it in as an image

// define your function for generating rotated text
writeRotatedText = function(text) {
  var ctx, canvas = document.createElement('canvas');
  // I am using predefined dimensions so either make this part of the arguments or change at will 
  canvas.width = 36;
  canvas.height = 270;
  ctx = canvas.getContext('2d');
  ctx.font = '36pt Arial';
  ctx.save();
  ctx.translate(36,270);
  ctx.rotate(-0.5*Math.PI);
  ctx.fillStyle = '#000';
  ctx.fillText(text , 0, 0);
  ctx.restore();
  return canvas.toDataURL();
};

// set the fitted width/height to a fraction for mitigating pixelation on print/zoom
var tableBody = [
 [{image: writeRotatedText('I am rotated'), fit:[7,53], alignment: 'center'}]
];

// use this body in a table definition

EDIT: Sorry guys, I thought this was about "vertically rotated text" since I was looking for that when I saw this issue. I'm leaving this reply here in case anyone still finds this useful.

@jorgebo10
Copy link

+1

@BubuInc
Copy link

BubuInc commented Feb 15, 2016

The only way I could do it was by setting margin-top. i.e.: margin: [0, 20, 0, 0]

@sirhcybe
Copy link

+1

strokovnjaka pushed a commit to strokovnjaka/pdfmake2 that referenced this issue May 23, 2016
strokovnjaka pushed a commit to strokovnjaka/pdfmake2 that referenced this issue May 23, 2016
@fguillen
Copy link

+1

@csvan
Copy link

csvan commented Sep 13, 2016

+1

sherpya added a commit to netfarm/pdfmake that referenced this issue Sep 14, 2016
sherpya added a commit to netfarm/pdfmake that referenced this issue Sep 14, 2016
@Dejab666
Copy link

+1

mattvoss pushed a commit to mattvoss/bitpdfmake that referenced this issue Sep 30, 2016
This was referenced Nov 28, 2016
@mehmetalidumlu
Copy link

+1

@kabirbaidhya
Copy link

kabirbaidhya commented Feb 28, 2017

I think this feature is really an important one. As we really need to tweak vertical alignment in the table cell most often. Is there any recent update on this issue?

@sheikirfanbasha
Copy link

+1

5 similar comments
@lerit
Copy link

lerit commented Apr 6, 2017

+1

@filipejoe
Copy link

+1

@olgaivanysko
Copy link

+1

@beebha
Copy link

beebha commented Jun 27, 2017

+1

@rdkleine
Copy link

+1

@danielrs95
Copy link

@bpampuch is there any plan to merge the PR mentioned above?

@smallg
Copy link

smallg commented Oct 10, 2022

@bpampuch any updates for this?

@albertoLGDev
Copy link

Hello, no update since last commit. Le mer. 1 juin 2022 à 22:29, Yanick Rochon @.> a écrit :

Any update on this? — Reply to this email directly, view it on GitHub <#74 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AETTDGCJFAEIB7F5LEW3JJTVM7BZFANCNFSM4ATC5LHQ . You are receiving this because you commented.Message ID: @.
>

Hello @Kran67,

I guess they didn`t put the request live to all of us and this thing is driving me crazy.
So would you mind sharing the code you managed to get this working?

It will be highly appreciated.

Thank you so much for your time.

@Fronikuniu
Copy link

Works fine for me

[
  {
    table: {
      widths: [imageContentSize],
      heights: [textContentSize],
      body: [
        [
          {
            text,
            style: {
              fontSize,
              alignment: 'center',
            },
          },
        ],
      ],
    },
    layout: {
      hLineWidth: () => borderWidth,
      vLineWidth: () => borderWidth,
      hLineColor: () => color,
      vLineColor: () => color,
      paddingTop: (index, node) => {
        const text = node.table.body[index][0]._inlines;
        if (text) {
          const textHeight = text[0].height;
          const margin = (textContentSize - textHeight) / 2;
          node.table.body[index].forEach((cell) => {
            if (cell._margin) {
              cell._margin[1] = margin;
            } else {
              cell._margin = [0, margin, 0, 0];
            }
          });
        }
        return 0;
      },
      paddingLeft: () => 0,
      paddingRight: () => 0,
      paddingBottom: () => 0,
    },
  },
],

@Msmldavies
Copy link

+1 would still love to have this feature. Any word on that pull request?

@SawkaDev
Copy link

SawkaDev commented Jun 8, 2023

+1

1 similar comment
@gee06robson
Copy link

+1

@mhdSid
Copy link

mhdSid commented Jul 7, 2023

This is really needed! I mean not having this feature is really bad DX.

@clezag
Copy link

clezag commented Jul 12, 2023

For anyone struggling with rowspans and vertical alignment:
The code above wasn't working with rowspan, so I've modified it a bit to cover my use case (rowspan + all cells centered).

rowSpanCell argument is an empty array that must persist between calls of the function, e.g. a var/const

function fillMargin(obj, margin) {
    if (margin) {
        obj.height += (margin[1] + margin[3])
        obj.width += (margin[0] + margin[2])
    }
    return obj
}
function findInlineHeight(cell, maxWidth, usedWidth = 0) {
    if (cell._margin) {
        maxWidth = maxWidth - cell._margin[0] - cell._margin[2]
    }
    let calcLines = (inlines) => {
        if (!inlines) {
            return {
                height: 0,
                width: 0
            }
        }
        let currentMaxHeight = 0
        let lastHadLineEnd = false
        for (const currentNode of inlines) {
            usedWidth += currentNode.width
            if (usedWidth > maxWidth || lastHadLineEnd) {
                currentMaxHeight += currentNode.height
                usedWidth = currentNode.width
            } else {
                currentMaxHeight = Math.max(currentNode.height, currentMaxHeight)
            }
            lastHadLineEnd = !!currentNode.lineEnd
        }
        return fillMargin({
            height: currentMaxHeight,
            width: usedWidth
        }, cell._margin)
    }
    if (cell._offsets) {
        usedWidth += cell._offsets.total
    }
    if (cell._inlines && cell._inlines.length) {
        return calcLines(cell._inlines)
    } else if (cell.stack && cell.stack[0]) {
        return fillMargin(cell.stack.map(item => {
            return findInlineHeight(item, maxWidth)
        }).reduce((prev, next) => {
            return {
                height: prev.height + next.height,
                width: Math.max(prev.width + next.width)
            }
        }), cell._margin)
    } else if (cell.ul) {
        return fillMargin(cell.ul.map(item => {
            return findInlineHeight(item, maxWidth)
        }).reduce((prev, next) => {
            return {
                height: prev.height + next.height,
                width: Math.max(prev.width + next.width)
            }
        }), cell._margin)
    } else if (cell.table) {
        let currentMaxHeight = 0
        for (const currentTableBodies of cell.table.body) {
            const innerTableHeights = currentTableBodies.map(mapTableBodies, maxWidth, usedWidth)
            currentMaxHeight = Math.max(...innerTableHeights, currentMaxHeight)
        }
        return fillMargin({
            height: currentMaxHeight,
            width: usedWidth
        }, cell._margin)
    } else if (cell._height) {
        usedWidth += cell._width
        return fillMargin({
            height: cell._height,
            width: usedWidth
        }, cell._margin)
    }

    return fillMargin({
        height: null,
        width: usedWidth
    }, cell._margin)
}

function updateRowSpanCell(rowHeight, rowSpanCell) {
    for (let i = rowSpanCell.length - 1; i >= 0; i--) {
        const rowCell = rowSpanCell[i]
        rowCell.maxHeight = rowCell.maxHeight + rowHeight
        const {
            maxHeight,
            cellHeight,
            align,
            cell
        } = rowCell
        rowCell.rowSpanCount = rowCell.rowSpanCount - 1
        if (!rowCell.rowSpanCount) {
            if (cellHeight && maxHeight > cellHeight) {
                let topMargin

                let cellAlign = align
                if (Array.isArray(align)) {
                    cellAlign = align
                }
                if (cellAlign === 'bottom') {
                    topMargin = maxHeight - cellHeight
                } else if (cellAlign === 'center') {
                    topMargin = (maxHeight - cellHeight) / 2
                }
                if (topMargin) {
                    if (cell._margin) {
                        cell._margin[1] = cell._margin[1] + topMargin
                    } else {
                        cell._margin = [0, topMargin, 0, 0]
                    }
                }
            }
            rowSpanCell.splice(i, 1)
        }
    }
}

function applyVerticalAlignment(node, rowIndex, align, rowSpanCell, manualHeight = 0) { // New default argument
    const allCellHeights = node.table.body[rowIndex].map(
        (innerNode, columnIndex) => {
            if (innerNode._span) return null
            const calcWidth = [...Array(innerNode.colSpan || 1).keys()].reduce((acc, i) => {
                return acc + node.table.widths[columnIndex + i]._calcWidth
            }, 0)
            const mFindInlineHeight = findInlineHeight(innerNode, calcWidth, 0, rowIndex, columnIndex)
            return mFindInlineHeight.height
        }
    )
    let maxRowHeight = manualHeight ? manualHeight[rowIndex] : Math.max(...allCellHeights) // handle manual height
    node.table.body[rowIndex].forEach((cell, ci) => {
        // rowSpan
        if (cell.rowSpan) {
            rowSpanCell.push({
                cell,
                rowSpanCount: cell.rowSpan,
                cellHeight: allCellHeights[ci],
                maxHeight: 0,
                align
            })
            return
        }
        if (allCellHeights[ci] && maxRowHeight > allCellHeights[ci]) {
            let topMargin

            let cellAlign = align
            if (Array.isArray(align)) {
                cellAlign = align[ci]
            }
            if (cellAlign === 'bottom') {
                topMargin = maxRowHeight - allCellHeights[ci]
            } else if (cellAlign === 'center') {
                topMargin = (maxRowHeight - allCellHeights[ci]) / 2
            }
            if (topMargin) {
                if (cell._margin) {
                    cell._margin[1] += topMargin
                } else {
                    cell._margin = [0, topMargin, 0, 0]
                }
            }
        }
    })
    updateRowSpanCell(maxRowHeight, rowSpanCell)
    if (rowSpanCell.length > 0) {    
        applyVerticalAlignment(node, rowIndex + 1, align, rowSpanCell, manualHeight)
    }
}

To explain a bit:
applyVerticalAlignment() is called by pdfmake for every single row prior to rendering it
It basically figures out the height of a cell and than modifies it's margin property to push the content to the center/bottom
When it encounters a cell with rowspan, instead of setting the margin immediately, it stores the cell in rowSpanCell, and only upon encountering the ending row of the rowspan, the margin of the cell is set.
Problem is: That original row containing the rowspan cell has already been rendered at that point, so setting the margin has no effect.
I've added a recursive call of applyVerticalAlignment() at the very end, so as long as we have unresolved rowspans, we look ahead to figure out the margin BEFORE rendering the row.

@mhdSid
Copy link

mhdSid commented Jul 12, 2023

It looks like you have just created another library just for vertical alignment

@FinalGhost
Copy link

As @clezag's code seems to depend on the functions mentioned elsewhere in this discussion, I have created a complete script based on his and other previous work. Therefore, his explanation still applies. As this might save some time, I thought I would post it here.
To use the following code add it in a new file, import CenteredLayout and just add: layout: CenteredLayout, to your table.

export const CenteredLayout = { paddingTop: function (index, node) {
    applyVerticalAlignment(node, index, 'center', []);
    return 0;
  },
  defaultBorder:false, }

function findInlineHeight(cell, maxWidth, usedWidth = 0, rowIndex?: number, columnIndex?: number) {
  if (cell._margin) {
    maxWidth = maxWidth - cell._margin[0] - cell._margin[2]
  }
  let calcLines = (inlines) => {
    if (!inlines) {
      return {
        height: 0,
        width: 0
      }
    }
    let currentMaxHeight = 0
    let lastHadLineEnd = false
    for (const currentNode of inlines) {
      usedWidth += currentNode.width
      if (usedWidth > maxWidth || lastHadLineEnd) {
        currentMaxHeight += currentNode.height
        usedWidth = currentNode.width
      } else {
        currentMaxHeight = Math.max(currentNode.height, currentMaxHeight)
      }
      lastHadLineEnd = !!currentNode.lineEnd
    }
    return fillMargin({
      height: currentMaxHeight,
      width: usedWidth
    }, cell._margin)
  }
  if (cell._offsets) {
    usedWidth += cell._offsets.total
  }
  if (cell._inlines && cell._inlines.length) {
    return calcLines(cell._inlines)
  } else if (cell.stack && cell.stack[0]) {
    return fillMargin(cell.stack.map(item => {
      return findInlineHeight(item, maxWidth)
    }).reduce((prev, next) => {
      return {
        height: prev.height + next.height,
        width: Math.max(prev.width + next.width)
      }
    }), cell._margin)
  } else if (cell.ul) {
    return fillMargin(cell.ul.map(item => {
      return findInlineHeight(item, maxWidth)
    }).reduce((prev, next) => {
      return {
        height: prev.height + next.height,
        width: Math.max(prev.width + next.width)
      }
    }), cell._margin)
  } else if (cell.table) {
    let currentMaxHeight = 0
    for (const currentTableBodies of cell.table.body) {
      const innerTableHeights = currentTableBodies.map((val) => mapTableBodies(val, maxWidth, usedWidth))
      currentMaxHeight = Math.max(...innerTableHeights, currentMaxHeight)
    }
    return fillMargin({
      height: currentMaxHeight,
      width: usedWidth
    }, cell._margin)
  } else if (cell._height) {
    usedWidth += cell._width
    return fillMargin({
      height: cell._height,
      width: usedWidth
    }, cell._margin)
  }

  return fillMargin({
    height: null,
    width: usedWidth
  }, cell._margin)
}

function mapTableBodies(innerTableCell, maxWidth, usedWidth) {
  const inlineHeight = findInlineHeight(
    innerTableCell,
    maxWidth,
    usedWidth
  );

  usedWidth = inlineHeight.width;
  return inlineHeight.height;
}

function fillMargin(obj, margin) {
  if (margin) {
    obj.height += (margin[1] + margin[3])
    obj.width += (margin[0] + margin[2])
  }
  return obj
}
function updateRowSpanCell(rowHeight, rowSpanCell) {
  for (let i = rowSpanCell.length - 1; i >= 0; i--) {
    const rowCell = rowSpanCell[i]
    rowCell.maxHeight = rowCell.maxHeight + rowHeight
    const {
      maxHeight,
      cellHeight,
      align,
      cell
    } = rowCell
    rowCell.rowSpanCount = rowCell.rowSpanCount - 1
    if (!rowCell.rowSpanCount) {
      if (cellHeight && maxHeight > cellHeight) {
        let topMargin

        let cellAlign = align
        if (Array.isArray(align)) {
          cellAlign = align
        }
        if (cellAlign === 'bottom') {
          topMargin = maxHeight - cellHeight
        } else if (cellAlign === 'center') {
          topMargin = (maxHeight - cellHeight) / 2
        }
        if (topMargin) {
          if (cell._margin) {
            cell._margin[1] = cell._margin[1] + topMargin
          } else {
            cell._margin = [0, topMargin, 0, 0]
          }
        }
      }
      rowSpanCell.splice(i, 1)
    }
  }
}

function applyVerticalAlignment(node, rowIndex, align, rowSpanCell, manualHeight = 0) { // New default argument
  const allCellHeights = node.table.body[rowIndex].map(
    (innerNode, columnIndex) => {
      if (innerNode._span) return null
      const calcWidth = [...Array(innerNode.colSpan || 1).keys()].reduce((acc, i) => {
        return acc + node.table.widths[columnIndex + i]._calcWidth
      }, 0)
      const mFindInlineHeight = findInlineHeight(innerNode, calcWidth, 0, rowIndex, columnIndex)
      return mFindInlineHeight.height
    }
  )
  let maxRowHeight = manualHeight ? manualHeight[rowIndex] : Math.max(...allCellHeights) // handle manual height
  node.table.body[rowIndex].forEach((cell, ci) => {
    // rowSpan
    if (cell.rowSpan) {
      rowSpanCell.push({
        cell,
        rowSpanCount: cell.rowSpan,
        cellHeight: allCellHeights[ci],
        maxHeight: 0,
        align
      })
      return
    }
    if (allCellHeights[ci] && maxRowHeight > allCellHeights[ci]) {
      let topMargin

      let cellAlign = align
      if (Array.isArray(align)) {
        cellAlign = align[ci]
      }
      if (cellAlign === 'bottom') {
        topMargin = maxRowHeight - allCellHeights[ci]
      } else if (cellAlign === 'center') {
        topMargin = (maxRowHeight - allCellHeights[ci]) / 2
      }
      if (topMargin) {
        if (cell._margin) {
          cell._margin[1] += topMargin
        } else {
          cell._margin = [0, topMargin, 0, 0]
        }
      }
    }
  })
  updateRowSpanCell(maxRowHeight, rowSpanCell)
  if (rowSpanCell.length > 0) {
    applyVerticalAlignment(node, rowIndex + 1, align, rowSpanCell, manualHeight)
  }
}

@kallekulp
Copy link

Now the only thing we need is for someone to map this to TypeScript and we would be golden. It's impressive that this question was asked 10 years ago and still no response...

@NunzioMatera
Copy link

After 10 fkn years, no updates? WOW

@kallekulp
Copy link

After investigating the source code for a day I strongly think this is not doable in TypeScript. A lot of the properties in the provided examples simply doesn't exist in the types for PdfMake.

@kklassno8
Copy link

How I solved the bottom edge alignment problem.

// Создаем массив для хранения результатов цикла
const employeeData = [];
response.forEach(employee => {
    // Создаем временный элемент для измерения ширины текста
    var tempElement = { offsetWidth: 0 };
    var tempText = employee.number_contract;

    // Имитируем измерение ширины текста путем подсчета символов
    for (var i = 0; i < tempText.length; i++) {
        tempElement.offsetWidth += 4; // Предполагаемая ширина символа (может потребоваться корректировка)
    }

    // Вычисляем количество строк, которое занимает текст в столбце employee.number_contract
    var columnWidth = 212; // Ширина столбца
    var numberContractLines = Math.ceil(tempElement.offsetWidth / columnWidth);

    // Генерируем необходимое количество знаков переноса строки для выравнивания
    var lineBreaks = '';
    for (var i = 0; i < numberContractLines; i++) {
        lineBreaks += '\n';
    }

// Удаляем один перенос	
var index = lineBreaks.indexOf('\n');
if (index !== -1) {
    lineBreaks = lineBreaks.slice(0, index) + lineBreaks.slice(index + 1);
}
    // Добавляем данные в таблицу с учетом вертикального выравнивания
    employeeData.push({
        columns: [
            { text: employee.number_contract, width: 212, lineHeight: 1, margin: [0, 0, 0, 0] },
            { text: lineBreaks + employee.hours, width: 65, alignment: 'center', lineHeight: 1, margin: [5, 0, 3, 0] },
            { text: lineBreaks + employee.value_ball, width: 92, alignment: 'center', lineHeight: 1, margin: [1, 0, 0, 0] }
        ],
    });
});

Unfortunately, I did not have time to figure out why an extra line break is being created, so I count the total number of line break characters and subtract one.

This is just a general concept, it hasn't been through much testing.

@Cauen
Copy link

Cauen commented Jun 3, 2024

Now the only thing we need is for someone to map this to TypeScript and we would be golden.

Based on @clezag and @FinalGhost code, ive replaced the code to typescript:

Click me to show the code
import type { CustomTableLayout } from "pdfmake/interfaces"

type CustomSize = {
	_calcWidth: number
}

type ContentTableCustom = {
	table: TableCustom
}

type TableCustom = {
	body: TableCellCustom[][]
	widths: CustomSize[]
}

type TableCellCustom = {
	_margin: [number, number, number, number] | null
	_inlines?: {
		text: string
		width: number
		height: number
		// dont know type yet
		lineEnd?: unknown
	}[]
	_offsets?: { total: number }
	stack?: TableCellCustom[]
	ul?: TableCellCustom[]
	table: TableCustom
	_height?: number
	_width?: number
	_span?: boolean
	colSpan?: number
	rowSpan?: number
}

type RowSpan = {
	cell: TableCellCustom
	rowSpanCount: number
	cellHeight: number | null
	maxHeight: number
	align: Align
}

export const CenteredLayout: CustomTableLayout = {
	paddingTop: (index, node) => {
		// @ts-expect-error 'TableCell' is not assignable to type 'TableCellCustom'
		applyVerticalAlignment(node, index, "center", [])
		return 0
	},

	// defaultBorder: false,
}

function findInlineHeight(
	cell: TableCellCustom,
	maxWidth: number,
	usedWidth = 0,
): { height: number | null; width: number | null } {
	if (cell._margin) {
		maxWidth = maxWidth - cell._margin[0] - cell._margin[2]
	}
	const calcLines = (inlines: TableCellCustom["_inlines"]) => {
		if (!inlines) {
			return {
				height: 0,
				width: 0,
			}
		}
		let currentMaxHeight = 0
		let lastHadLineEnd = false
		for (const currentNode of inlines) {
			usedWidth += currentNode.width
			if (usedWidth > maxWidth || lastHadLineEnd) {
				currentMaxHeight += currentNode.height
				usedWidth = currentNode.width
			} else {
				currentMaxHeight = Math.max(currentNode.height, currentMaxHeight)
			}
			lastHadLineEnd = !!currentNode.lineEnd
		}
		return fillMargin(
			{
				height: currentMaxHeight,
				width: usedWidth,
			},
			cell._margin,
		)
	}

	if (cell._offsets) {
		usedWidth += cell._offsets.total
	}

	if (cell._inlines?.length) {
		return calcLines(cell._inlines)
	}
	if (cell.stack?.[0]) {
		return fillMargin(
			cell.stack
				.map((item: TableCellCustom) => {
					return findInlineHeight(item, maxWidth)
				})
				.reduce((prev, next) => {
					const prevHeight = prev.height || 0
					const nextHeight = next.height || 0
					const prevWidth = prev.width || 0
					const nextWidth = next.width || 0
					return {
						height: prevHeight + nextHeight,
						width: Math.max(prevWidth + nextWidth),
					}
				}),
			cell._margin,
		)
	}
	if (cell.ul) {
		return fillMargin(
			cell.ul
				.map((item) => {
					return findInlineHeight(item, maxWidth)
				})
				.reduce((prev, next) => {
					const prevHeight = prev.height || 0
					const nextHeight = next.height || 0
					const prevWidth = prev.width || 0
					const nextWidth = next.width || 0
					return {
						height: prevHeight + nextHeight,
						width: Math.max(prevWidth + nextWidth),
					}
				}),
			cell._margin,
		)
	}
	if (cell.table) {
		let currentMaxHeight = 0
		for (const currentTableBodies of cell.table.body) {
			const innerTableHeights = currentTableBodies
				.map((val) => mapTableBodies(val, maxWidth, usedWidth))
				.filter(isTruthy)
			currentMaxHeight = Math.max(...innerTableHeights, currentMaxHeight)
		}
		return fillMargin(
			{
				height: currentMaxHeight,
				width: usedWidth,
			},
			cell._margin,
		)
	}
	if (cell._height && cell._width) {
		usedWidth += cell._width
		return fillMargin(
			{
				height: cell._height,
				width: usedWidth,
			},
			cell._margin,
		)
	}

	return fillMargin(
		{
			height: null,
			width: usedWidth,
		},
		cell._margin,
	)
}

function mapTableBodies(
	innerTableCell: TableCellCustom,
	maxWidth: number,
	usedWidth: number,
): number | null {
	const inlineHeight = findInlineHeight(innerTableCell, maxWidth, usedWidth)
	if (inlineHeight.width) {
		usedWidth = inlineHeight.width
	}
	return inlineHeight.height
}

function fillMargin(
	obj: { height: number | null; width: number | null },
	margin: [number, number, number, number] | null,
): { height: number | null; width: number | null } {
	// [ts] Margin always null?
	if (margin && obj.height && obj.width) {
		obj.height += margin[1] + margin[3]
		obj.width += margin[0] + margin[2]
	}
	return obj
}

type Align = "top" | "center" | "bottom" | [number, number]

function isTruthy<T>(value?: T | undefined | null | false): value is T {
	return !!value
}

function applyVerticalAlignment(
	node: ContentTableCustom,
	rowIndex: number,
	align: Align,
	rowSpanCell: RowSpan[],
) {
	// New default argument
	const allCellHeights = node.table.body[rowIndex]
		.map((innerNode, columnIndex) => {
			if (innerNode._span) return null
			const calcWidth = [...Array(innerNode.colSpan || 1).keys()].reduce(
				(acc, i) => {
					return acc + node.table.widths?.[columnIndex + i]._calcWidth
				},
				0,
			)
			const mFindInlineHeight = findInlineHeight(innerNode, calcWidth, 0)
			return mFindInlineHeight.height
		})
		.filter(isTruthy)
	const maxRowHeight = Math.max(...allCellHeights.filter(isTruthy))
	node.table.body[rowIndex].forEach((cell, ci) => {
		// rowSpan
		if (cell.rowSpan) {
			rowSpanCell.push({
				cell,
				rowSpanCount: cell.rowSpan,
				cellHeight: allCellHeights[ci],
				maxHeight: 0,
				align,
			})
			return
		}
		if (allCellHeights[ci] && maxRowHeight > allCellHeights[ci]) {
			const cellAlign = align

			const topMargin = (() => {
				if (cellAlign === "bottom") return maxRowHeight - allCellHeights[ci]
				return (maxRowHeight - allCellHeights[ci]) / 2
			})()
			if (topMargin) {
				if (cell._margin) {
					cell._margin[1] += topMargin
				} else {
					cell._margin = [0, topMargin, 0, 0]
				}
			}
		}
	})

	function updateRowSpanCell(rowHeight: number, rowSpanCell: RowSpan[]) {
		for (let i = rowSpanCell.length - 1; i >= 0; i--) {
			const rowCell = rowSpanCell[i]
			rowCell.maxHeight = rowCell.maxHeight + rowHeight
			const { maxHeight, cellHeight, align, cell } = rowCell
			rowCell.rowSpanCount = rowCell.rowSpanCount - 1
			if (!rowCell.rowSpanCount) {
				if (cellHeight && maxHeight > cellHeight) {
					const topMargin = (() => {
						if (align === "bottom") return maxHeight - cellHeight
						if (align === "center") return (maxHeight - cellHeight) / 2
						return undefined
					})()

					if (topMargin) {
						if (cell._margin) {
							cell._margin[1] = cell._margin[1] + topMargin
						} else {
							cell._margin = [0, topMargin, 0, 0]
						}
					}
				}
				rowSpanCell.splice(i, 1)
			}
		}
	}

	updateRowSpanCell(maxRowHeight, rowSpanCell)

	if (rowSpanCell.length > 0) {
		applyVerticalAlignment(node, rowIndex + 1, align, rowSpanCell)
	}
}

Unfortunately, this code didn't align exactly to the middle in my tests and changed the original height of the lines. But it gave me a good idea of ​​how to implement a solution.

Typing this code was the first step towards the definitive workaround solution.

I believe the solution must come from a paddingTop function that checks the cells' internal data and returns a custom numeric value based on the index. Without causing collateral effects on the system by changing this internal data.

@R1D3R175
Copy link

R1D3R175 commented Jul 3, 2024

+1

4 similar comments
@thehouseisonfire
Copy link

+1

@nikzanda
Copy link

+1

@ElhananGitHub
Copy link

+1

@JulienLecoq
Copy link

+1

@calebdinsmore
Copy link

Happy 10-year (+2 days) anniversary of this issue! 🎉

It's been years since I've maintained anything depending on this library, but I nonetheless plan to have champagne to celebrate the day this feature is added

@ahadnawaz585
Copy link

ahadnawaz585 commented Aug 22, 2024

To address this, I developed a custom utility function that adjusts the vertical alignment of table columns based on the length of the memo column. While this method might not be the most conventional, it provided a practical solution for my specific needs.

Utility Function: VerticalAlign

The VerticalAlign function calculates the vertical margin needed to align columns by considering the length of the text and the maximum characters allowed per line.

export function VerticalAlign(data: string, length: number): number[] {
    const difference = data.length - length;
    const multiple = Math.ceil(difference / length); // Example multiple
  
    console.log(`Difference: ${difference}`);
    console.log(`Multiple: ${multiple}`);
  
    if (data.length > length) {
      return [0, 5 * multiple, 0, 0]; 
    } else {
      return [0, 0, 0, 0];
    }
}

Explanation

  • Parameters:

    • data: The text data for which alignment is needed.
    • length: The maximum number of characters that can fit in a single line.
  • Logic:

    • The function calculates the difference between the actual text length and the allowed length.
    • It then determines how many additional lines are needed by dividing this difference by the maximum length and rounding up.
    • Based on this, it returns the vertical margin to adjust the alignment.

Usage in Table

Here's how the VerticalAlign function is used to set margins in a table:

{
    text: entry.date ? formatDate(new Date(entry.date).toString()) : "",
    style: "tableBody2",
    margin: VerticalAlign(entry.memo, 55)
},
{
    text: entry.memo,
    style: "tableBody2",
},
{
    text: entry.debit && entry.debit !== 0 && entry.debit !== 0.00
      ? entry.debit.toLocaleString()
      : "",
    style: "tableBody",
    margin: VerticalAlign(entry.memo, 55)
},
{
    text: entry.credit && entry.credit !== 0 && entry.credit !== 0.00
      ? entry.credit.toLocaleString()
      : "",
    style: "tableBody",
    margin: VerticalAlign(entry.memo, 55)
},
{
    text: entry.balance !== null ? entry.balance.toLocaleString() : "",
    style: "tableBody",
    margin: VerticalAlign(entry.memo, 55)
},

By using the VerticalAlign function, we were able to ensure that columns remain aligned even when text wraps across multiple lines. This solution, while tailored to our specific use case, can be adapted for similar alignment challenges in pdfmake.It worked for me.

@Yuta4u
Copy link

Yuta4u commented Sep 12, 2024

hello world, any update?

@hujjatx
Copy link

hujjatx commented Sep 25, 2024

+1

1 similar comment
@arthurvergacas
Copy link

+1

@t0m-4
Copy link

t0m-4 commented Oct 29, 2024

we made a fork for that #2436

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests