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: