/* _CPTableTextAttachment.j * A self-contained, renderable text attachment representation of a table. * * Copyright (C) 2026 Daniel Boehringer * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import "CPView.j" @import "CPTextView.j" @import "CPTextField.j" @import @implementation _CPTableTextAttachment : CPView { CPArray _headers; CPArray _rows; } - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows { return [self initWithHeaders:headers rows:rows width:500.0]; } - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth { self = [super initWithFrame:CGRectMake(0, 0, totalWidth, 20)]; if (self) { _headers = headers; _rows = rows; var numCols = [headers count]; if (numCols == 0 && [rows count] > 0) numCols = [[rows objectAtIndex:0] count]; // Apply borders on the outer left and top container edges if (self._DOMElement) { self._DOMElement.style.borderTop = "1px solid #e0e0e0"; self._DOMElement.style.borderLeft = "1px solid #e0e0e0"; } // Initialize header cells if ([headers count] > 0) { for (var c = 0; c < numCols; c++) { var headerText = [headers objectAtIndex:c]; var cellView = [self createCellWithText:headerText frame:CGRectMakeZero() isHeader:YES]; [self addSubview:cellView]; } } // Initialize body rows for (var r = 0; r < [rows count]; r++) { var rowData = [rows objectAtIndex:r]; for (var c = 0; c < numCols; c++) { var cellText = @""; if (c < [rowData count]) cellText = [rowData objectAtIndex:c]; var cellView = [self createCellWithText:cellText frame:CGRectMakeZero() isHeader:NO]; [self addSubview:cellView]; } } [self resizeToWidth:totalWidth]; } return self; } - (CPArray)headers { return _headers; } - (CPArray)rows { return _rows; } - (CPView)viewForWidth:(float)width { [self resizeToWidth:width]; return self; } - (CPView)createCellWithText:(CPString)text frame:(CGRect)frame isHeader:(BOOL)isHeader { var initialWidth = (frame.size.width > 0) ? frame.size.width : 120.0; var initialHeight = (frame.size.height > 0) ? frame.size.height : 28.0; var cellContainer = [[CPView alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, initialWidth, initialHeight)]; [cellContainer setBackgroundColor:isHeader ? [CPColor colorWithWhite:0.92 alpha:1.0] : [CPColor whiteColor]]; // Bottom and right edge borders to construct the grid var borderView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, initialWidth, initialHeight)]; [borderView setBackgroundColor:[CPColor clearColor]]; [borderView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; if (borderView._DOMElement) { borderView._DOMElement.style.borderBottom = "1px solid #e0e0e0"; borderView._DOMElement.style.borderRight = "1px solid #e0e0e0"; borderView._DOMElement.style.boxSizing = "border-box"; } [cellContainer addSubview:borderView]; var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)]; var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer]; [textView setEditable:NO]; [textView setSelectable:YES]; [textView setBackgroundColor:[CPColor clearColor]]; [textView setVerticallyResizable:YES]; [textView setHorizontallyResizable:NO]; [[textView textContainer] setWidthTracksTextView:YES]; // Construct cell text using standard fonts var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0]; var parsedText = [[CPAttributedString alloc] initWithString:text attributes:@{ CPFontAttributeName: cellFont, CPForegroundColorAttributeName: [CPColor blackColor] }]; var storage = [textView textStorage]; if (storage && [storage respondsToSelector:@selector(setAttributedString:)]) { [storage setAttributedString:parsedText]; } else { [textView setEditable:YES]; [textView setString:@""]; [textView insertText:parsedText]; [textView setEditable:NO]; } [cellContainer addSubview:textView]; return cellContainer; } - (CPTextView)getTextViewFromCell:(CPView)cellView { var subviews = [cellView subviews]; for (var i = 0; i < [subviews count]; i++) { var sub = [subviews objectAtIndex:i]; if ([sub isKindOfClass:[CPTextView class]]) { return sub; } } return nil; } - (void)resizeToWidth:(float)newWidth { var numCols = [_headers count]; if (numCols == 0 && [_rows count] > 0) { numCols = [[_rows objectAtIndex:0] count]; } if (numCols == 0) return; var subviews = [self subviews]; var colNaturalWidths = []; var colMinWidths = []; for (var c = 0; c < numCols; c++) { colNaturalWidths[c] = 80.0; colMinWidths[c] = 60.0; } var measureTextField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 10000.0, 24.0)]; [measureTextField setFont:[CPFont systemFontOfSize:13.0]]; var measureCell = function(cellText, isHeader, colIndex) { var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0]; [measureTextField setFont:cellFont]; [measureTextField setStringValue:cellText]; [measureTextField sizeToFit]; var naturalW = CGRectGetWidth([measureTextField frame]) + 24.0; if (naturalW > colNaturalWidths[colIndex]) { colNaturalWidths[colIndex] = naturalW; } var words = cellText.split(/[\s\-]/); var maxWordW = 50.0; for (var w = 0; w < words.length; w++) { var word = words[w].trim(); if (word.length === 0) continue; [measureTextField setStringValue:word]; [measureTextField sizeToFit]; var wordW = CGRectGetWidth([measureTextField frame]) + 30.0; if (wordW > maxWordW) { maxWordW = wordW; } } if (maxWordW > colMinWidths[colIndex]) { colMinWidths[colIndex] = maxWordW; } }; for (var c = 0; c < [_headers count]; c++) { measureCell([_headers objectAtIndex:c], YES, c); } for (var r = 0; r < [_rows count]; r++) { var rowData = [_rows objectAtIndex:r]; for (var c = 0; c < numCols; c++) { var cellText = @""; if (c < [rowData count]) { cellText = [rowData objectAtIndex:c]; } measureCell(cellText, NO, c); } } var totalMinWidth = 0.0; for (var c = 0; c < numCols; c++) { totalMinWidth += colMinWidths[c]; } var colWidths = []; if (newWidth <= totalMinWidth) { var remainingWidth = newWidth; for (var c = 0; c < numCols; c++) { var w = Math.floor((colMinWidths[c] / totalMinWidth) * newWidth); colWidths[c] = w; remainingWidth -= w; } if (numCols > 0) colWidths[numCols - 1] += remainingWidth; } else { for (var c = 0; c < numCols; c++) { colWidths[c] = colMinWidths[c]; } var totalGrowthCapacity = 0.0; var growthCapacities = []; for (var c = 0; c < numCols; c++) { var capacity = Math.max(0.0, colNaturalWidths[c] - colMinWidths[c]); growthCapacities[c] = capacity; totalGrowthCapacity += capacity; } var extraWidth = newWidth - totalMinWidth; var remainingExtra = extraWidth; for (var c = 0; c < numCols; c++) { if (totalGrowthCapacity > 0) { var w = Math.floor((growthCapacities[c] / totalGrowthCapacity) * extraWidth); colWidths[c] += w; remainingExtra -= w; } } if (numCols > 0) { colWidths[numCols - 1] += remainingExtra; } } var cellIndex = 0; var currentY = 0; var layoutRow = function(startIndex) { var maxCellHeight = 28.0; for (var c = 0; c < numCols; c++) { var idx = startIndex + c; if (idx < [subviews count]) { var cellView = [subviews objectAtIndex:idx]; var textView = [self getTextViewFromCell:cellView]; if (textView) { var targetWidth = Math.max(10.0, colWidths[c] - 8); [[textView textContainer] setContainerSize:CGSizeMake(targetWidth, 1e7)]; var usedRect = [[textView layoutManager] usedRectForTextContainer:[textView textContainer]]; var wrappedHeight = CGRectGetHeight(usedRect) + 12.0; if (wrappedHeight > maxCellHeight) { maxCellHeight = wrappedHeight; } } } } var currentX = 0; for (var c = 0; c < numCols; c++) { var idx = startIndex + c; if (idx < [subviews count]) { var cellView = [subviews objectAtIndex:idx]; [cellView setFrame:CGRectMake(currentX, currentY, colWidths[c], maxCellHeight)]; var textView = [self getTextViewFromCell:cellView]; if (textView) { var targetWidth = Math.max(10.0, colWidths[c] - 8); var textY = 4.0; var finalTextViewHeight = maxCellHeight - 8.0; [textView setFrame:CGRectMake(4, textY, targetWidth, finalTextViewHeight)]; } var cellSubviews = [cellView subviews]; if ([cellSubviews count] > 0) { [[cellSubviews objectAtIndex:0] setFrame:CGRectMake(0, 0, colWidths[c], maxCellHeight)]; } } currentX += colWidths[c]; } return maxCellHeight; }; if ([_headers count] > 0) { var headerHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += headerHeight; } for (var r = 0; r < [_rows count]; r++) { var rowHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += rowHeight; } [self setFrameSize:CGSizeMake(newWidth, currentY)]; } @end