-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Comments
not at the moment, this would be a little bit problematic to support all boundary cases |
Yes. to be able to align text to the bottom border of a table cell would be useful. |
+1 |
1 similar comment
+1 |
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. |
+1 |
The only way I could do it was by setting margin-top. i.e.: margin: [0, 20, 0, 0] |
+1 |
+1 |
+1 |
+1 |
+1 |
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? |
+1 |
5 similar comments
+1 |
+1 |
+1 |
+1 |
+1 |
@bpampuch is there any plan to merge the PR mentioned above? |
@bpampuch any updates for this? |
Hello @Kran67, I guess they didn`t put the request live to all of us and this thing is driving me crazy. It will be highly appreciated. Thank you so much for your time. |
Works fine for me
|
+1 would still love to have this feature. Any word on that pull request? |
+1 |
1 similar comment
+1 |
This is really needed! I mean not having this feature is really bad DX. |
For anyone struggling with rowspans and vertical alignment:
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: |
It looks like you have just created another library just for vertical alignment |
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. 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)
}
} |
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... |
After 10 fkn years, no updates? WOW |
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. |
How I solved the bottom edge alignment problem.
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. |
Based on @clezag and @FinalGhost code, ive replaced the code to typescript: Click me to show the codeimport 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 |
+1 |
4 similar comments
+1 |
+1 |
+1 |
+1 |
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 |
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:
|
hello world, any update? |
+1 |
1 similar comment
+1 |
we made a fork for that #2436 |
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.
The text was updated successfully, but these errors were encountered: