|
const { warn, debug } = require('./debug'); |
|
const Cell = require('./cell'); |
|
const { ColSpanCell, RowSpanCell } = Cell; |
|
|
|
(function () { |
|
function next(alloc, col) { |
|
if (alloc[col] > 0) { |
|
return next(alloc, col + 1); |
|
} |
|
return col; |
|
} |
|
|
|
function layoutTable(table) { |
|
let alloc = {}; |
|
table.forEach(function (row, rowIndex) { |
|
let col = 0; |
|
row.forEach(function (cell) { |
|
cell.y = rowIndex; |
|
|
|
cell.x = rowIndex ? next(alloc, col) : col; |
|
const rowSpan = cell.rowSpan || 1; |
|
const colSpan = cell.colSpan || 1; |
|
if (rowSpan > 1) { |
|
for (let cs = 0; cs < colSpan; cs++) { |
|
alloc[cell.x + cs] = rowSpan; |
|
} |
|
} |
|
col = cell.x + colSpan; |
|
}); |
|
Object.keys(alloc).forEach((idx) => { |
|
alloc[idx]--; |
|
if (alloc[idx] < 1) delete alloc[idx]; |
|
}); |
|
}); |
|
} |
|
|
|
function maxWidth(table) { |
|
let mw = 0; |
|
table.forEach(function (row) { |
|
row.forEach(function (cell) { |
|
mw = Math.max(mw, cell.x + (cell.colSpan || 1)); |
|
}); |
|
}); |
|
return mw; |
|
} |
|
|
|
function maxHeight(table) { |
|
return table.length; |
|
} |
|
|
|
function cellsConflict(cell1, cell2) { |
|
let yMin1 = cell1.y; |
|
let yMax1 = cell1.y - 1 + (cell1.rowSpan || 1); |
|
let yMin2 = cell2.y; |
|
let yMax2 = cell2.y - 1 + (cell2.rowSpan || 1); |
|
let yConflict = !(yMin1 > yMax2 || yMin2 > yMax1); |
|
|
|
let xMin1 = cell1.x; |
|
let xMax1 = cell1.x - 1 + (cell1.colSpan || 1); |
|
let xMin2 = cell2.x; |
|
let xMax2 = cell2.x - 1 + (cell2.colSpan || 1); |
|
let xConflict = !(xMin1 > xMax2 || xMin2 > xMax1); |
|
|
|
return yConflict && xConflict; |
|
} |
|
|
|
function conflictExists(rows, x, y) { |
|
let i_max = Math.min(rows.length - 1, y); |
|
let cell = { x: x, y: y }; |
|
for (let i = 0; i <= i_max; i++) { |
|
let row = rows[i]; |
|
for (let j = 0; j < row.length; j++) { |
|
if (cellsConflict(cell, row[j])) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
function allBlank(rows, y, xMin, xMax) { |
|
for (let x = xMin; x < xMax; x++) { |
|
if (conflictExists(rows, x, y)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
function addRowSpanCells(table) { |
|
table.forEach(function (row, rowIndex) { |
|
row.forEach(function (cell) { |
|
for (let i = 1; i < cell.rowSpan; i++) { |
|
let rowSpanCell = new RowSpanCell(cell); |
|
rowSpanCell.x = cell.x; |
|
rowSpanCell.y = cell.y + i; |
|
rowSpanCell.colSpan = cell.colSpan; |
|
insertCell(rowSpanCell, table[rowIndex + i]); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
function addColSpanCells(cellRows) { |
|
for (let rowIndex = cellRows.length - 1; rowIndex >= 0; rowIndex--) { |
|
let cellColumns = cellRows[rowIndex]; |
|
for (let columnIndex = 0; columnIndex < cellColumns.length; columnIndex++) { |
|
let cell = cellColumns[columnIndex]; |
|
for (let k = 1; k < cell.colSpan; k++) { |
|
let colSpanCell = new ColSpanCell(); |
|
colSpanCell.x = cell.x + k; |
|
colSpanCell.y = cell.y; |
|
cellColumns.splice(columnIndex + 1, 0, colSpanCell); |
|
} |
|
} |
|
} |
|
} |
|
|
|
function insertCell(cell, row) { |
|
let x = 0; |
|
while (x < row.length && row[x].x < cell.x) { |
|
x++; |
|
} |
|
row.splice(x, 0, cell); |
|
} |
|
|
|
function fillInTable(table) { |
|
let h_max = maxHeight(table); |
|
let w_max = maxWidth(table); |
|
debug(`Max rows: ${h_max}; Max cols: ${w_max}`); |
|
for (let y = 0; y < h_max; y++) { |
|
for (let x = 0; x < w_max; x++) { |
|
if (!conflictExists(table, x, y)) { |
|
let opts = { x: x, y: y, colSpan: 1, rowSpan: 1 }; |
|
x++; |
|
while (x < w_max && !conflictExists(table, x, y)) { |
|
opts.colSpan++; |
|
x++; |
|
} |
|
let y2 = y + 1; |
|
while (y2 < h_max && allBlank(table, y2, opts.x, opts.x + opts.colSpan)) { |
|
opts.rowSpan++; |
|
y2++; |
|
} |
|
let cell = new Cell(opts); |
|
cell.x = opts.x; |
|
cell.y = opts.y; |
|
warn(`Missing cell at ${cell.y}-${cell.x}.`); |
|
insertCell(cell, table[y]); |
|
} |
|
} |
|
} |
|
} |
|
|
|
function generateCells(rows) { |
|
return rows.map(function (row) { |
|
if (!Array.isArray(row)) { |
|
let key = Object.keys(row)[0]; |
|
row = row[key]; |
|
if (Array.isArray(row)) { |
|
row = row.slice(); |
|
row.unshift(key); |
|
} else { |
|
row = [key, row]; |
|
} |
|
} |
|
return row.map(function (cell) { |
|
return new Cell(cell); |
|
}); |
|
}); |
|
} |
|
|
|
function makeTableLayout(rows) { |
|
let cellRows = generateCells(rows); |
|
layoutTable(cellRows); |
|
fillInTable(cellRows); |
|
addRowSpanCells(cellRows); |
|
addColSpanCells(cellRows); |
|
return cellRows; |
|
} |
|
|
|
module.exports = { |
|
makeTableLayout: makeTableLayout, |
|
layoutTable: layoutTable, |
|
addRowSpanCells: addRowSpanCells, |
|
maxWidth: maxWidth, |
|
fillInTable: fillInTable, |
|
computeWidths: makeComputeWidths('colSpan', 'desiredWidth', 'x', 1), |
|
computeHeights: makeComputeWidths('rowSpan', 'desiredHeight', 'y', 1), |
|
}; |
|
})(); |
|
|
|
function makeComputeWidths(colSpan, desiredWidth, x, forcedMin) { |
|
return function (vals, table) { |
|
let result = []; |
|
let spanners = []; |
|
let auto = {}; |
|
table.forEach(function (row) { |
|
row.forEach(function (cell) { |
|
if ((cell[colSpan] || 1) > 1) { |
|
spanners.push(cell); |
|
} else { |
|
result[cell[x]] = Math.max(result[cell[x]] || 0, cell[desiredWidth] || 0, forcedMin); |
|
} |
|
}); |
|
}); |
|
|
|
vals.forEach(function (val, index) { |
|
if (typeof val === 'number') { |
|
result[index] = val; |
|
} |
|
}); |
|
|
|
|
|
for (let k = spanners.length - 1; k >= 0; k--) { |
|
let cell = spanners[k]; |
|
let span = cell[colSpan]; |
|
let col = cell[x]; |
|
let existingWidth = result[col]; |
|
let editableCols = typeof vals[col] === 'number' ? 0 : 1; |
|
if (typeof existingWidth === 'number') { |
|
for (let i = 1; i < span; i++) { |
|
existingWidth += 1 + result[col + i]; |
|
if (typeof vals[col + i] !== 'number') { |
|
editableCols++; |
|
} |
|
} |
|
} else { |
|
existingWidth = desiredWidth === 'desiredWidth' ? cell.desiredWidth - 1 : 1; |
|
if (!auto[col] || auto[col] < existingWidth) { |
|
auto[col] = existingWidth; |
|
} |
|
} |
|
|
|
if (cell[desiredWidth] > existingWidth) { |
|
let i = 0; |
|
while (editableCols > 0 && cell[desiredWidth] > existingWidth) { |
|
if (typeof vals[col + i] !== 'number') { |
|
let dif = Math.round((cell[desiredWidth] - existingWidth) / editableCols); |
|
existingWidth += dif; |
|
result[col + i] += dif; |
|
editableCols--; |
|
} |
|
i++; |
|
} |
|
} |
|
} |
|
|
|
Object.assign(vals, result, auto); |
|
for (let j = 0; j < vals.length; j++) { |
|
vals[j] = Math.max(forcedMin, vals[j] || 0); |
|
} |
|
}; |
|
} |
|
|