Answered
m1bCorrect answer
Community Expert
Apr 11, 2026 7:30 AM UTC
Hi
I’ve written a script that does it on the test file from your related question and it works, but you will see there are some complications, and the code runs a bit slow because it is testing the cell heights after each scaling test. Still it does the job I think.
— Mark

/**
* @file Expand Table Rows To Fill Frame.js
*
* @author m1b
* @version 2026-05-05
* @discussion https://community.adobe.com/questions-671/how-to-quickly-adjust-the-bodyrow-to-align-to-the-textframe-bottom-1554513
*/
function main() {
const APPLY_TO_WHOLE_THREAD = true;
const IGNORE_LAST_FRAME = true;
var doc = app.activeDocument;
var items = APPLY_TO_WHOLE_THREAD ? getAllThreadedFrames(doc.selection) : doc.selection;
if (APPLY_TO_WHOLE_THREAD && IGNORE_LAST_FRAME)
items.pop();
for (var i = 0; i < items.length; i++)
expandTableRowsToFillFrame(items[i], true);
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Expand Rows To Fill Frame');
/**
* Scales the rows of the last table in text frame
* to their maximum, without overflowing.
* @author m1b
* @version 2026-05-04
* @param {TextFrame} textFrame - the target text frame.
* @param {Boolean} [onlyBodyRows] - whether to scale only body rows (default: true).
*/
function expandTableRowsToFillFrame(textFrame, onlyBodyRows) {
app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;
onlyBodyRows = false === onlyBodyRows
if (TextFrame !== textFrame.constructor)
return;
var tf;
var row;
var table = null;
if (
textFrame.characters.lastItem().isValid
&& '\u0016' === textFrame.characters.lastItem().contents
)
table = textFrame.characters.lastItem().tables[0];
if (!textFrame.characters.lastItem().isValid) {
// this text frame might have a table, but not the first part of it
var table = null;
var timeout = 20;
var prev = textFrame.previousTextFrame;
// search for table in previous frames
while (prev && !table && timeout--) {
$.writeln('prev.name = ' + prev.name);
if (
prev.characters.lastItem().isValid
&& '\u0016' === prev.characters.lastItem().contents
)
table = prev.characters.lastItem().tables[0];
prev = prev.previousTextFrame;
}
}
if (!table)
return;
var allRows = table.rows;
var frameRows = [];
var frameHeights = [];
for (var i = 0; i < allRows.length; i++) {
row = allRows[i];
tf = getParentTextFrame(row, false);
if (!tf)
// row is likely overflowing
break;
if (tf === textFrame) {
// found one!
frameRows.push(row);
frameHeights.push(row.height);
}
else if (frameRows.length)
// no need to keep looking
break;
}
// now we have the rows we want to target
var lastRow = frameRows[frameRows.length - 1];
// find an upper bound where rows overflow the frame
var low = 1;
var high = 2;
while (scaleRows(high))
high *= 2;
// binary search for the largest scaleFactor that keeps rows in-frame
var iterations = 50;
var precision = 0.001;
while (high - low > precision && iterations--) {
var mid = (low + high) / 2;
if (scaleRows(mid))
low = mid;
else
high = mid;
}
// apply the best scale
scaleRows(low);
/**
* Scales original height rows by `scaleFactor`, and returns true, when the frameRows still fit in frame.
* @param {Number} scaleFactor - multiply the original heights by this.
* @returns {Boolean}
*/
function scaleRows(scaleFactor) {
for (var i = 0; i < frameRows.length; i++)
if (RowTypes.BODY_ROW === frameRows[i].rowType)
frameRows[i].height = frameHeights[i] * scaleFactor;
// if it fits, return true
return getParentTextFrame(lastRow, false) === textFrame;
};
};
/**
* Return a row's parent text frame.
*
* Notes:
*
* (1) Getting the parent text frame of an overset cell is a
* challenge because there are no insertion points. Here I
* add a zero-width space as the first character so at least
* the first insertionPoint will now reside in the text frame.
*
* (2) Another complication is a bug where indesign will
* display the incorrect height for an autogrowing, overflowing
* cell, and place the cell in the wrong parent text frame.
* (See explanation here: https://community.adobe.com/t5/indesign-discussions/how-can-get-parenttextframe-of-a-cell-was-overflow/m-p/14030313
*
* (3) If your use case is such that returning `undefined` when the
* cell|row is overset is acceptable, then pass `false` to the
* `evenWhenCellIsOverset` parameter. This will be much faster.
*
* @author m1b
* @version 2025-04-05
*
* @param {Cell|Row} row - an Indesign Table Row or Cell.
* @param {Boolean} [evenWhenCellIsOverset] - whether to find the parent text frame of an overset cell (default: true).
* @returns {TextFrame}
*/
function getParentTextFrame(row, evenWhenCellIsOverset) {
if (!row.isValid)
return;
if (
false === evenWhenCellIsOverset
&& row.hasOwnProperty('texts')
)
// the quick way; will return `undefined` if the cell is overset
return row.texts[0].parentTextFrames[0];
if (row.constructor.name == 'Cell')
row = row.rows[0];
var cells = row.cells;
var rowTextFrameIDs = {};
var rowTextFrame;
var parentTextFrames = row;
// find the text frames of the row
while (
!parentTextFrames.hasOwnProperty('parentStory')
|| undefined == parentTextFrames.parent
)
parentTextFrames = parentTextFrames.parent;
if (!parentTextFrames.hasOwnProperty('parentStory'))
return;
parentTextFrames = parentTextFrames.parentStory.textContainers;
for (var i = 0; i < cells.length; i++) {
var cell = cells[i];
// insert zero-width space to expose first
// insertion point in case where cell is overset
cell.insertionPoints[0].contents = '\u200B';
if (cell.insertionPoints[0].parentTextFrames.length > 0)
// store the parent frame index of this cell
rowTextFrameIDs[cell.insertionPoints[0].parentTextFrames[0].id] = true;
// clean up
cell.characters[0].remove();
}
// match it to the *last-most* parentTextFrame to ensure
// that the entire row returns the same (correct) text frame
parentFrameLoop:
for (var j = parentTextFrames.length - 1; j >= 0; j--) {
if (rowTextFrameIDs[parentTextFrames[j].id] == true) {
rowTextFrame = parentTextFrames[j];
break parentFrameLoop;
}
}
if (
rowTextFrame != undefined
&& rowTextFrame.isValid
)
return rowTextFrame;
};
/**
* Returns all text frames in items, and all text frames threaded with those.
* @author m1b
* @version 2026-05-04
* @param {*} items - the items to inspect.
* @returns {Array<TextFrame>}
*/
function getAllThreadedFrames(items) {
var frames = [];
var unique = {};
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (
'TextFrame' !== item.constructor.name
|| unique[item.id]
)
continue;
frames.push(item);
unique[item.id] = true;
var next = item.nextTextFrame;
var prev = item.previousTextFrame;
while (
prev
&& prev.isValid
) {
if (!unique[prev.id]) {
frames.push(prev);
unique[prev.id] = true;
}
prev = prev.previousTextFrame;
}
while (
next
&& next.isValid
) {
if (!unique[next.id]) {
frames.push(next);
unique[next.id] = true;
}
next = next.nextTextFrame;
}
}
return frames;
};Edit 2026-05-04: fixed bug where script didn’t target every text frame in a thread. Added two constants to configure the process.

