Skip to content
173 changes: 39 additions & 134 deletions lib/src/builtins/flutter_html_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_layout_grid/flutter_layout_grid.dart';

import '../tables/fixed_headers/fixed_headers_table_widget.dart';
import '../tables/table_helper.dart';

/// [TableHtmlExtension] adds support for the <table> element to the flutter_html library.
/// <tr>, <tbody>, <tfoot>, <thead>, <th>, <td>, <col>, and <colgroup> are also
/// supported.
Expand All @@ -14,7 +17,14 @@ import 'package:flutter_layout_grid/flutter_layout_grid.dart';
///
///
class TableHtmlExtension extends HtmlExtension {
const TableHtmlExtension();
/// It use shrinkWrap option to CssBoxWidgets. But in build method it fill available width.
/// Use only if something wrong with the view.
/// Uses shrinkWrap option in many widgets can be very expensive, because it means that you have to measure everything.
final bool shrinkAndFill;
bool hasFixedHeaders = false;
final tableHelper = TableHelper();

TableHtmlExtension({this.shrinkAndFill = false});

@override
Set<String> get supportedTags => {
Expand All @@ -32,6 +42,8 @@ class TableHtmlExtension extends HtmlExtension {
@override
StyledElement prepare(ExtensionContext context, List<StyledElement> children) {
if (context.elementName == "table") {
hasFixedHeaders = context.attributes.containsKey("data-fixed-header");

final cellDescendants = _getCellDescendants(children);

return TableElement(
Expand Down Expand Up @@ -105,6 +117,7 @@ class TableHtmlExtension extends HtmlExtension {
if (context.elementName == "table") {
return WidgetSpan(
child: CssBoxWidget(
shrinkWrap: shrinkAndFill,
style: context.styledElement!.style,
child: LayoutBuilder(
builder: (ctx, constraints) {
Expand All @@ -114,12 +127,25 @@ class TableHtmlExtension extends HtmlExtension {
} else {
width = MediaQuery.sizeOf(ctx).width - 32;
}
return _layoutCells(
context.styledElement as TableElement,
context.builtChildrenMap!,
context,
width,
);
if (hasFixedHeaders) {
return FixedHeadersTableWidget(
table: context.styledElement as TableElement,
parsedCells: context.builtChildrenMap!,
context: context,
width: width,
shrinkWrap: shrinkAndFill,
tableHelper: tableHelper,
);
} else {
return _layoutCells(
context.styledElement as TableElement,
context.builtChildrenMap!,
context,
width,
shrinkAndFill,
tableHelper,
);
}
},
),
),
Expand Down Expand Up @@ -151,15 +177,16 @@ List<TableCellElement> _getCellDescendants(List<StyledElement> children) {
return descendants;
}

Widget _layoutCells(TableElement table, Map<StyledElement, InlineSpan> parsedCells, ExtensionContext context, double width) {
final minWidths = _getColWidths(table.tableStructure);
Widget _layoutCells(
TableElement table, Map<StyledElement, InlineSpan> parsedCells, ExtensionContext context, double width, bool shrinkAndFill, TableHelper tableHelper) {
final minWidths = tableHelper.getColWidths(table.tableStructure);
double requiredWidth = 0;
for (final minWidth in minWidths) {
requiredWidth += minWidth;
}

List<double> cellWidths;
if (requiredWidth < width) {
if (shrinkAndFill || requiredWidth < width) {
final extra = (width - requiredWidth) / minWidths.length;
cellWidths = List.generate(minWidths.length, (index) => minWidths[index] + extra);
} else {
Expand Down Expand Up @@ -219,12 +246,13 @@ Widget _layoutCells(TableElement table, Map<StyledElement, InlineSpan> parsedCel
rowStart: rowi,
rowSpan: min(child.rowspan, rows.length - rowi),
child: CssBoxWidget(
shrinkWrap: shrinkAndFill,
style: child.style.merge(row.style),
child: Builder(builder: (context) {
final alignment = child.style.direction ?? Directionality.of(context);
return SizedBox.expand(
child: Container(
alignment: _getCellAlignment(child, alignment),
alignment: tableHelper.getCellAlignment(child, alignment),
child: CssBoxWidget.withInlineSpanChildren(
children: [parsedCells[child] ?? const TextSpan(text: "error")],
style: Style(),
Expand Down Expand Up @@ -266,129 +294,6 @@ Widget _layoutCells(TableElement table, Map<StyledElement, InlineSpan> parsedCel
));
}

List<double> _getColWidths(List<StyledElement> children) {
final widths = <double>[];
for (final child in children) {
List<double> partialWidths = [];
if (child is TableRowLayoutElement) {
partialWidths = _getColWidthsFromRow(child);
} else {
partialWidths = _getColWidths(child.children);
}
if (partialWidths.isEmpty) continue;
for (int i = 0; i < partialWidths.length; ++i) {
double partial = partialWidths[i];
if (widths.length <= i) {
widths.add(partial);
} else if (widths[i] < partial) {
widths[i] = partial;
}
}
}
return widths;
}

List<double> _getColWidthsFromRow(TableRowLayoutElement row) {
List<double> widths = [];
for (final cell in row.children) {
if (cell is TableCellElement) {
WidthInfo info = WidthInfo();
for (final child in cell.children) {
_getCellInfo(child, info);
}
double minWidth = info.requiredWidth + 32;
widths.add(minWidth);
}
}
return widths;
}

void _getCellInfo(StyledElement element, WidthInfo info) {
if (element is TextContentElement) {
final regex = RegExp(r'\w+|\s+|[^\w\s]');
final wordRegex = RegExp(r'\w+');
final text = element.text;
if (text == null || text.isEmpty) return;
final words = regex.allMatches(text).map((m) => m.group(0)!).toList();
for (final word in words) {
double wordWidth = TextPainter.computeWidth(
text: TextSpan(
text: word,
style: TextStyle(
fontSize: element.style.fontSize?.value ?? 16,
fontFamily: element.style.fontFamily,
fontWeight: element.style.fontWeight,
fontStyle: element.style.fontStyle,
)),
textDirection: TextDirection.ltr,
);
if (info.join && wordRegex.hasMatch(word)) {
info.width += wordWidth;
} else {
info.width = wordWidth;
}
if (info.width > info.requiredWidth) {
info.requiredWidth = info.width;
}
info.join = wordRegex.hasMatch(word);
}
} else {
for (final child in element.children) {
_getCellInfo(child, info);
}
}
}

class WidthInfo {
double width = 0;
double requiredWidth = 0;
bool join = false;
}

Alignment _getCellAlignment(TableCellElement cell, TextDirection alignment) {
Alignment verticalAlignment;

switch (cell.style.verticalAlign) {
case VerticalAlign.baseline:
case VerticalAlign.sub:
case VerticalAlign.sup:
case VerticalAlign.top:
verticalAlignment = Alignment.topCenter;
break;
case VerticalAlign.middle:
verticalAlignment = Alignment.center;
break;
case VerticalAlign.bottom:
verticalAlignment = Alignment.bottomCenter;
break;
}

switch (cell.style.textAlign) {
case TextAlign.left:
return verticalAlignment + Alignment.centerLeft;
case TextAlign.right:
return verticalAlignment + Alignment.centerRight;
case TextAlign.center:
return verticalAlignment + Alignment.center;
case null:
case TextAlign.start:
case TextAlign.justify:
switch (alignment) {
case TextDirection.rtl:
return verticalAlignment + Alignment.centerRight;
case TextDirection.ltr:
return verticalAlignment + Alignment.centerLeft;
}
case TextAlign.end:
switch (alignment) {
case TextDirection.rtl:
return verticalAlignment + Alignment.centerLeft;
case TextDirection.ltr:
return verticalAlignment + Alignment.centerRight;
}
}
}

class TableCellElement extends StyledElement {
int colspan = 1;
int rowspan = 1;
Expand Down
29 changes: 3 additions & 26 deletions lib/src/builtins/styled_element_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -390,18 +390,9 @@ class StyledElementBuiltIn extends HtmlExtension {
case "strong":
continue bold;
case "sub":
if (_hasOnlyDigits(children)) {
// We can use FontFeature.subscripts since this just looks nice
styledElement.style = Style(
fontFeatureSettings: [const FontFeature.subscripts()],
);
} else {
// For the rest we should just reduce the height of the text
// Using WidgetSpan with translation should not be used because line breaks can occur just before subscript text
styledElement.style = Style(
fontSize: FontSize(50, Unit.percent),
);
}
styledElement.style = Style(
fontSize: FontSize(50, Unit.percent),
);
break;
case "summary":
styledElement.style = Style(
Expand Down Expand Up @@ -429,20 +420,6 @@ class StyledElementBuiltIn extends HtmlExtension {
return styledElement;
}

bool _hasOnlyDigits(List<StyledElement> children) {
if (children.isEmpty) return true;
for (var child in children) {
if (child is TextContentElement) {
final text = child.text;
if (text == null || text.isEmpty) continue;
for (int codeUnit in text.codeUnits) {
if ((codeUnit ^ 0x30) > 9) return false;
}
}
}
return true;
}

@override
InlineSpan build(ExtensionContext context) {
if (context.styledElement!.style.display == Display.listItem ||
Expand Down
25 changes: 16 additions & 9 deletions lib/src/css_box_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,23 @@ class CssBoxWidget extends StatelessWidget {
Container(
decoration: BoxDecoration(
border: style.border,
color: style.backgroundColor, //Colors the padding and content boxes
borderRadius: style.borderRadius,
),
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: style.borderRadius,
color: style.backgroundColor, //Colors the padding and content boxes
),
width: _shouldExpandToFillBlock() ? double.infinity : null,
padding: padding,
child: top
? child
: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child,
),
),
width: _shouldExpandToFillBlock() ? double.infinity : null,
padding: padding,
child: top
? child
: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child,
),
),
if (markerBox != null) ClickableRichText(text: markerBox),
],
Expand Down
40 changes: 40 additions & 0 deletions lib/src/css_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,29 @@ Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
style.border = ExpressionMapping.expressionToBorder(
borderWidths, borderStyles, borderColors);
break;
case 'border-radius':
List<css.LiteralTerm?>? borderRadiuses =
value.whereType<css.LiteralTerm>().toList();

/// List<css.LiteralTerm> might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
borderRadiuses.removeWhere((element) =>
element == null ||
(element is! css.LengthTerm &&
element is! css.PercentageTerm &&
element is! css.EmTerm &&
element is! css.RemTerm &&
element is! css.NumberTerm));

css.LiteralTerm? borderRadius =
borderRadiuses.firstWhereOrNull((element) => element != null);

BorderRadius newBorderRadius = BorderRadius.all(Radius.circular(
ExpressionMapping.expressionToBorderRadius(
borderRadius)),

);
style.borderRadius = newBorderRadius;
break;
case 'border-left':
List<css.LiteralTerm?>? borderWidths =
value.whereType<css.LiteralTerm>().toList();
Expand Down Expand Up @@ -875,6 +898,23 @@ class ExpressionMapping {
return BorderStyle.none;
}

static double expressionToBorderRadius(css.Expression? value) {
if (value is css.NumberTerm) {
return double.tryParse(value.text) ?? 1.0;
} else if (value is css.PercentageTerm) {
return (double.tryParse(value.text) ?? 400) / 100;
} else if (value is css.EmTerm) {
return double.tryParse(value.text) ?? 1.0;
} else if (value is css.RemTerm) {
return double.tryParse(value.text) ?? 1.0;
} else if (value is css.LengthTerm) {
return double.tryParse(
value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), '')) ??
1.0;
}
return 4.0;
}

static Color? expressionToColor(css.Expression? value) {
if (value != null) {
if (value is css.HexColorTerm) {
Expand Down
Loading