diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05d1fd9070..2aecac6619 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## [2.2.0] - November 29, 2021:
+* Explicitly declare multiplatform support
+* Extended and fixed list-style (marker) support
+* Basic support for height/width css properties
+* Support changing scroll physics of SelectableText.rich
+* Support text transform css property
+* Bumped minimum flutter_math_fork version for Flutter 2.5 compatibility
+* Fix styling of iframes
+* Fix nested font tag application
+* Fix whitespace rendering between list items
+* Prevent crash on empty
tag and tables with both colspan/rowspan
+* Prevent crash on use of negative margins in css
+
## [2.1.5] - October 7, 2021:
* Ignore unsupported custom style selectors when using fromCss
* Fix SVG tag usage inside tables
diff --git a/README.md b/README.md
index 66dff54e73..d1a04f11f6 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
Add the following to your `pubspec.yaml` file:
dependencies:
- flutter_html: ^2.1.5
+ flutter_html: ^2.2.0
## Currently Supported HTML Tags:
| | | | | | | | | | | |
diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart
index 6d8d38e45e..eb714e8ccd 100644
--- a/lib/flutter_html.dart
+++ b/lib/flutter_html.dart
@@ -7,7 +7,7 @@ import 'package:flutter_html/image_render.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
-import 'package:webview_flutter/webview_flutter.dart';
+import 'package:flutter_html/src/navigation_delegate.dart';
//export render context api
export 'package:flutter_html/html_parser.dart';
@@ -18,6 +18,7 @@ export 'package:flutter_html/src/interactable_element.dart';
export 'package:flutter_html/src/layout_element.dart';
export 'package:flutter_html/src/replaced_element.dart';
export 'package:flutter_html/src/styled_element.dart';
+export 'package:flutter_html/src/navigation_delegate.dart';
//export style api
export 'package:flutter_html/style.dart';
diff --git a/lib/html_parser.dart b/lib/html_parser.dart
index c01f6b1f6a..d612ce9d1f 100644
--- a/lib/html_parser.dart
+++ b/lib/html_parser.dart
@@ -13,12 +13,12 @@ import 'package:flutter_html/src/anchor.dart';
import 'package:flutter_html/src/css_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/layout_element.dart';
+import 'package:flutter_html/src/navigation_delegate.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as htmlparser;
import 'package:numerus/numerus.dart';
-import 'package:webview_flutter/webview_flutter.dart';
typedef OnTap = void Function(
String? url,
@@ -737,7 +737,6 @@ class HtmlParser extends StatelessWidget {
String marker = "";
switch (tree.style.listStyleType!) {
case ListStyleType.NONE:
- tree.style.markerContent = '';
break;
case ListStyleType.CIRCLE:
marker = '○';
@@ -959,7 +958,7 @@ class HtmlParser extends StatelessWidget {
if (child is EmptyContentElement || child is EmptyLayoutElement) {
toRemove.add(child);
} else if (child is TextContentElement
- && tree.name == "body"
+ && (tree.name == "body" || tree.name == "ul")
&& child.text!.replaceAll(' ', '').isEmpty) {
toRemove.add(child);
} else if (child is TextContentElement
@@ -1054,7 +1053,7 @@ class ContainerSpan extends StatelessWidget {
height: style.height,
width: style.width,
padding: style.padding,
- margin: style.margin,
+ margin: style.margin?.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity)),
alignment: shrinkWrap ? null : style.alignment,
child: child ??
StyledText(
diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart
index b677e9d872..43d79cbc2a 100644
--- a/lib/src/layout_element.dart
+++ b/lib/src/layout_element.dart
@@ -100,6 +100,7 @@ class TableLayoutElement extends LayoutElement {
// Place the cells in the rows/columns
final cells = [];
final columnRowOffset = List.generate(columnMax, (_) => 0);
+ final columnColspanOffset = List.generate(columnMax, (_) => 0);
int rowi = 0;
for (var row in rows) {
int columni = 0;
@@ -107,11 +108,11 @@ class TableLayoutElement extends LayoutElement {
if (columni > columnMax - 1 ) {
break;
}
- while (columnRowOffset[columni] > 0) {
- columnRowOffset[columni] = columnRowOffset[columni] - 1;
- columni++;
- }
if (child is TableCellElement) {
+ while (columnRowOffset[columni] > 0) {
+ columnRowOffset[columni] = columnRowOffset[columni] - 1;
+ columni += columnColspanOffset[columni].clamp(1, columnMax - columni - 1);
+ }
cells.add(GridPlacement(
child: Container(
width: double.infinity,
@@ -139,6 +140,7 @@ class TableLayoutElement extends LayoutElement {
rowSpan: min(child.rowspan, rows.length - rowi),
));
columnRowOffset[columni] = child.rowspan - 1;
+ columnColspanOffset[columni] = child.colspan;
columni += child.colspan;
}
}
@@ -155,6 +157,11 @@ class TableLayoutElement extends LayoutElement {
max(0, columnMax - finalColumnSizes.length),
(_) => IntrinsicContentTrackSize());
+ if (finalColumnSizes.isEmpty || rowSizes.isEmpty) {
+ // No actual cells to show
+ return SizedBox();
+ }
+
return LayoutGrid(
gridFit: GridFit.loose,
columnSizes: finalColumnSizes,
diff --git a/lib/src/navigation_delegate.dart b/lib/src/navigation_delegate.dart
new file mode 100644
index 0000000000..d45bb9ed32
--- /dev/null
+++ b/lib/src/navigation_delegate.dart
@@ -0,0 +1,35 @@
+import 'dart:async';
+
+/// Information about a navigation action that is about to be executed.
+class NavigationRequest {
+ NavigationRequest({required this.url, required this.isForMainFrame});
+
+ /// The URL that will be loaded if the navigation is executed.
+ final String url;
+
+ /// Whether the navigation request is to be loaded as the main frame.
+ final bool isForMainFrame;
+
+ @override
+ String toString() {
+ return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)';
+ }
+}
+
+/// A decision on how to handle a navigation request.
+enum NavigationDecision {
+ /// Prevent the navigation from taking place.
+ prevent,
+
+ /// Allow the navigation to take place.
+ navigate,
+}
+
+/// Decides how to handle a specific navigation request.
+///
+/// The returned [NavigationDecision] determines how the navigation described by
+/// `navigation` should be handled.
+///
+/// See also: [WebView.navigationDelegate].
+typedef FutureOr NavigationDelegate(
+ NavigationRequest navigation);
diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart
index 6c3002dc97..a8527d4eeb 100644
--- a/lib/src/replaced_element.dart
+++ b/lib/src/replaced_element.dart
@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/anchor.dart';
import 'package:flutter_html/src/html_elements.dart';
+import 'package:flutter_html/src/navigation_delegate.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/src/widgets/iframe_unsupported.dart'
if (dart.library.io) 'package:flutter_html/src/widgets/iframe_mobile.dart'
@@ -16,7 +17,6 @@ import 'package:flutter_math_fork/flutter_math.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:html/dom.dart' as dom;
import 'package:video_player/video_player.dart';
-import 'package:webview_flutter/webview_flutter.dart';
/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered.
///
diff --git a/lib/src/widgets/iframe_mobile.dart b/lib/src/widgets/iframe_mobile.dart
index 96b3991cbc..b1e9fbe71c 100644
--- a/lib/src/widgets/iframe_mobile.dart
+++ b/lib/src/widgets/iframe_mobile.dart
@@ -2,9 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
+import 'package:flutter_html/src/navigation_delegate.dart';
import 'package:flutter_html/src/replaced_element.dart';
import 'package:flutter_html/style.dart';
-import 'package:webview_flutter/webview_flutter.dart';
+import 'package:webview_flutter/webview_flutter.dart' as webview;
import 'package:html/dom.dart' as dom;
/// [IframeContentElement is a [ReplacedElement] with web content.
@@ -33,13 +34,23 @@ class IframeContentElement extends ReplacedElement {
child: ContainerSpan(
style: context.style,
newContext: context,
- child: WebView(
+ child: webview.WebView(
initialUrl: src,
key: key,
javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts"
- ? JavascriptMode.unrestricted
- : JavascriptMode.disabled,
- navigationDelegate: navigationDelegate,
+ ? webview.JavascriptMode.unrestricted
+ : webview.JavascriptMode.disabled,
+ navigationDelegate: (request) async {
+ final result = await navigationDelegate!(NavigationRequest(
+ url: request.url,
+ isForMainFrame: request.isForMainFrame,
+ ));
+ if (result == NavigationDecision.prevent) {
+ return webview.NavigationDecision.prevent;
+ } else {
+ return webview.NavigationDecision.navigate;
+ }
+ },
gestureRecognizers: {
Factory(() => VerticalDragGestureRecognizer())
},
diff --git a/lib/src/widgets/iframe_unsupported.dart b/lib/src/widgets/iframe_unsupported.dart
index 4adae1a5d2..38c96eb0ab 100644
--- a/lib/src/widgets/iframe_unsupported.dart
+++ b/lib/src/widgets/iframe_unsupported.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
+import 'package:flutter_html/src/navigation_delegate.dart';
import 'package:flutter_html/src/replaced_element.dart';
import 'package:flutter_html/style.dart';
-import 'package:webview_flutter/webview_flutter.dart';
import 'package:html/dom.dart' as dom;
/// [IframeContentElement is a [ReplacedElement] with web content.
@@ -30,4 +30,4 @@ class IframeContentElement extends ReplacedElement {
child: Text("Iframes are currently not supported in this environment"),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/src/widgets/iframe_web.dart b/lib/src/widgets/iframe_web.dart
index 1000e778a1..cf68c54449 100644
--- a/lib/src/widgets/iframe_web.dart
+++ b/lib/src/widgets/iframe_web.dart
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/shims/dart_ui.dart' as ui;
+import 'package:flutter_html/src/navigation_delegate.dart';
import 'package:flutter_html/src/replaced_element.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/style.dart';
-import 'package:webview_flutter/webview_flutter.dart';
import 'package:html/dom.dart' as dom;
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
diff --git a/lib/style.dart b/lib/style.dart
index 3dfcd94457..fc2c3f6d21 100644
--- a/lib/style.dart
+++ b/lib/style.dart
@@ -548,20 +548,7 @@ class ListStyleType {
static const LOWER_ROMAN = ListStyleType("LOWER_ROMAN");
static const UPPER_ROMAN = ListStyleType("UPPER_ROMAN");
static const SQUARE = ListStyleType("SQUARE");
-}
-
-enum ListStyleType {
- LOWER_ALPHA,
- UPPER_ALPHA,
- LOWER_LATIN,
- UPPER_LATIN,
- CIRCLE,
- DISC,
- DECIMAL,
- LOWER_ROMAN,
- UPPER_ROMAN,
- SQUARE,
- NONE,
+ static const NONE = ListStyleType("NONE");
}
enum ListStylePosition {
diff --git a/pubspec.yaml b/pubspec.yaml
index 3b94c125e8..587187bcf9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: flutter_html
description: A Flutter widget rendering static HTML and CSS as Flutter widgets.
-version: 2.1.5
+version: 2.2.0
homepage: https://github.com/Sub6Resources/flutter_html
environment: