Skip to main content
dublove
Legend
April 28, 2025
Answered

ExtendScript: How to convert selected table rows to Header?

  • April 28, 2025
  • 1 reply
  • 1725 views

My table header usually has 1-4 rows, most of the time 1-2rows.

I sometimes need to determine which table header rows already exist.
Next I need to convert to body rows,  or copy the table header rows.

 

Before I applied the style, I determined the number of table headers by manually selecting them.
The table header rows are the rows I selected.
How should this code be expressed?

 

After applying table styles, if I ever have table headers.
How do I determine which rows are table headers?

Correct answer m1b

Hi @m1b 

I apologize for the trouble.

The preceding thought is a bit confusing.

 

Maybe I should have phrased it like this:
Judge this table, if there is no table header, convert the selected row to a table header.
If there is already a header, and record which rows are headers (I'm stubborn), then cancel the header.


Okay @dublove you just want a script to simply convert the selected cells to header rows and other rows to body rows. Easy right? Haha, here you go...

- Mark

 

/**
 * @file Convert Table Row Types.js
 *
 * Will convert the selected rows to header rows
 * and also convert any non-selected header rows
 * to body rows.
 *
 * Note: will convert the minimum number of rows
 * necessary to obey the rules around converting rows
 * with merged cells. This may be *more* than the
 * selected rows.
 *
 * @author m1b
 * @version 2025-07-18
 * @discussion https://community.adobe.com/t5/indesign-discussions/how-can-i-accurately-perform-a-table-header-row-conversion-operation-with-a-script/m-p/15295606
 */
function main() {

    var doc = app.activeDocument,
        selectedRows = getRows(doc.selection[0]);

    if (0 === selectedRows.length)
        return alert('Please select some table rows and try again.')

    // convert the selected rows to header rows
    var headerRows = convertToRowType(selectedRows, RowTypes.HEADER_ROW);

    // collect all the rows that now *should* be body rows
    var bodyRows = [],
        table = headerRows[0].parent,
        lastHeaderRowIndex = headerRows[headerRows.length - 1].index;

    for (var i = lastHeaderRowIndex + 1; i < table.rows.length; i++)
        if (RowTypes.HEADER_ROW === table.rows[i].rowType)
            bodyRows.push(table.rows[i]);

    // convert them to body rows
    var bodyRows = convertToRowType(bodyRows, RowTypes.BODY_ROW);

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Convert to Header Rows');

/**
 * Converts each row in `rows` to the given `rowType`.
 * Note that this will expand the rows to include all merged rows
 * because you can't set the rowType of a partially merged row.
 * @author m1b
 * @version 2025-05-01
 * @param {Array<Row>} rows - the target rows.
 * @param {RowTypes} rowType - the desired row type.
 * @returns {Array<Row>} - the rows that were converted.
 */
function convertToRowType(rows, rowType) {

    if (0 === rows.length)
        return [];

    var merges = [],
        table = rows[0].parent;

    // this will expand the rows to include all merged rows
    // because you can't set the rowType of a partially merged row
    rows = getRows(rows, undefined, true);

    rows.sort(
        // footers must be converted in reverse order
        (RowTypes.FOOTER_ROW === rowType)
            ? function (a, b) { return b.index - a.index }
            : function (a, b) { return a.index - b.index }
    );

    if (RowTypes.HEADER_ROW === rowType) {

        // because header rows must be at the start of the table
        // add any rows until the start
        while (rows[0].index > 0)
            rows.unshift(table.rows[rows[0].index - 1]);

    }

    else if (RowTypes.FOOTER_ROW === rowType) {

        // because footer rows must be at the end of the table,
        // add rows until the end
        while (rows[rows.length - 1].index + rows.length < table.rows.length)
            rows.unshift(table.rows[rows[rows.length - 1].index + rows.length]);

    }

    // find and unmerge cells that span across the selected rows
    for (var i = 0; i < rows.length; i++) {

        var row = rows[i];

        cellsLoop:
        for (var j = 0; j < row.cells.length; j++) {

            var cell = row.cells[j];
            var rs = cell.rowSpan;
            var cs = cell.columnSpan;

            if (1 === rs && 1 === cs)
                // not merged
                continue cellsLoop;

            merges.push({
                row: cell.parentRow.index,
                column: cell.parentColumn.index,
                rowSpan: rs,
                columnSpan: cs
            });

            cell.unmerge();

        }

    }

    // set the rowType of each row
    for (var i = 0; i < rows.length; i++)
        if (rows[i].rowType !== rowType)
            rows[i].rowType = rowType;

    // re-merge the previously merged cells
    for (var i = merges.length - 1; i >= 0; i--) {

        var m = merges[i],
            topLeft = table.rows[m.row].cells[m.column],
            bottomRight = table.rows[m.row + m.rowSpan - 1].cells[m.column + m.columnSpan - 1];

        topLeft.merge(bottomRight);

    }

    return rows;

};

/**
 * Returns the merged rows of a given row.
 * @author m1b
 * @version 2025-05-01
 * @param {Row} row - the target row.
 * @param {Function} [isUnique] - private function to check for uniqueness.
 * @returns {Array<Row>}
 */
function getMergedRows(row, isUnique) {

    isUnique = isUnique || uniqueChecker();

    var rows = [],
        rowIndex = row.index,
        table = row.parent;

    if (isUnique(row.index))
        rows.push(row);

    // 1. check for spans going down from this row
    var cells = table.rows[rowIndex].cells;
    for (var i = 0; i < cells.length; i++)
        for (var j = 0; j < cells[i].rowSpan; j++)
            if (isUnique(table.rows[rowIndex + j].index))
                rows.push(table.rows[rowIndex + j]);

    // 2. check for cells in earlier rows that reach into this one
    for (var r = 0, testRow; r < rowIndex; r++) {

        testRow = table.rows[r];
        span = testRow.rowSpan;

        if (r + span - 1 < rowIndex)
            continue;

        for (var j = 0; j < span; j++)
            if (isUnique(table.rows[r + j].index))
                rows.push(table.rows[r + j]);

    }

    rows.sort(function (a, b) { return a.index - b.index });

    return rows;

};

/**
 * Given cells, returns the cells' rows.
 * @author m1b
 * @version 2025-07-19
 * @param {Cell|Cells|Array<Row>} cells - the cells to get rows from.
 * @param {RowTypes} rowType - the desired row type.
 * @param {Boolean} [includeMergedRows] - whether to include merged rows (default: false).
 * @param {Table} [table] - the rows' table.
 * @param {Function} [isUnique] - private function to check for uniqueness.
 * @returns {Array<Row>}
 */
function getRows(cells, rowType, includeMergedRows, table, isUnique) {

    var rows = [],
        isUnique = isUnique || uniqueChecker();

    if (!cells)
        return;

    if ('function' === typeof cells.getElements)
        cells = cells.getElements();

    if (
        'Array' === cells.constructor.name
        && cells[0].hasOwnProperty('cells')
    ) {
        // to handle the case of expanding an array of rows to include merged rows
        for (var i = 0; i < cells.length; i++)
            rows = rows.concat(getRows(cells[i].cells, rowType, includeMergedRows, table, isUnique))

        return rows;
    }

    if (
        cells.hasOwnProperty('cells')
        && cells.cells.length > 0
    )
        cells = cells.cells;

    if (
        !cells.hasOwnProperty('0')
        || !cells[0].hasOwnProperty('parentRow')
    )
        // can't get rows from `cells`
        return [];

    var table = table || cells[0].parent;

    while (
        'Table' !== table.constructor.name
        && table.hasOwnProperty('parent')
    )
        table = table.parent;

    for (var i = 0, row; i < cells.length; i++) {

        spanLoop:
        for (var j = 0; j < cells[i].parentRow.rowSpan; j++) {

            row = table.rows[cells[i].parentRow.index + j];

            if (
                undefined != rowType
                && rowType !== row.rowType
            )
                continue spanLoop;

            if (includeMergedRows)
                rows = rows.concat(getMergedRows(row, isUnique));

            else if (isUnique(row.index))
                rows.push(row);

        }

    }

    return rows;

};

/**
 * Returns a function that, given a String, will
 * return true if that String has not been seen before.
 *
 * Example:
 *   var isUnique = uniqueChecker();
 *   if (isUnique(myThing))
 *     // do something with myThing
 *
 * @author m1b
 * @version 2025-05-01
 * @returns {Function}
 */
function uniqueChecker() {

    var unique = {};

    return function (str) {

        if (unique[str])
            return false;

        unique[str] = true;
        return true;

    };

};

Edit 2025-07-18: fixed bug in getRows function.

Edit 2025-08-26: updated getRows function.

1 reply

m1b
Community Expert
Community Expert
April 28, 2025

Hi @dublove here is a quick example.

- Mark

 

(function () {

    var doc = app.activeDocument,
        table = doc.stories.everyItem().tables.everyItem().getElements()[0];

    // we only want one header row
    var headerCount = 1;

    // loop backwards because you can't convert a header row
    // to a body row if there is a header row after it
    for (var r = table.rows.length - 1; r >= headerCount; r--) {

        if (RowTypes.HEADER_ROW === table.rows[r].rowType)
            // convert header row to body row
            table.rows[r].rowType = RowTypes.BODY_ROW;

    }

})();
dublove
dubloveAuthor
Legend
April 28, 2025

Hi @m1b 

You're too  fast.
I reworked my idea.

 

Before preparing to apply the table style,
I also wanted to determine the number of header rows in the current table by mouse selection.

Thank you.

m1b
Community Expert
Community Expert
April 28, 2025

Haha, okay how about this:

/**
 * @7111211 m1b
 */
(function () {

    var doc = app.activeDocument,
        table = getTable(doc.selection[0]);

    if (!table || !table.isValid)
        return alert('Please select a table and try again.');

    $.writeln('headerRowCount = ' + table.headerRowCount);
    $.writeln('bodyRowCount = ' + table.bodyRowCount);

})();

/**
 * Attempts to return a Table, given an object.
 * @7111211 m1b
 * @version 2023-02-06
 * @9397041 {InsertionPoint|Text|Cells|Table|TextFrame} obj - an object related to a Cell.
 * @Returns {Cell}
 */
function getTable(obj) {

    try {
        if (undefined == obj)
            return;
    } catch (error) {
        // Indesign bug - not sure why this sometimes occurs.
        // Reverting or re-opening document fixes it.
        return alert('Table found, but was temporarily invalid. Please close and re-open document and try again.');
    }

    if ('Array' === obj.constructor.name) {
        for (var i = 0, table; i < obj.length; i++) {
            table = getTable(obj[i]);
            if (table && table.isValid)
                return table;
        }
        return;
    }

    if (obj.constructor.name == 'Cell')
        return obj.parent;

    if (obj.parent.constructor.name == 'Cell')
        return obj.parent.parent;

    if (
        obj.hasOwnProperty('cells')
        && obj.cells.length > 0
    )
        return obj.cells[0].parent;

    if (
        obj.hasOwnProperty('tables')
        && 0 !== obj.tables.length
    )
        return obj.tables[0];

};