diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 06b6985c8f..b90e8459ee 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -3,6 +3,7 @@ library flutter_html; import 'package:flutter/material.dart'; import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/style.dart'; +import 'package:webview_flutter/webview_flutter.dart'; class Html extends StatelessWidget { /// The `Html` widget takes HTML as input and displays a RichText @@ -40,6 +41,7 @@ class Html extends StatelessWidget { this.onImageTap, this.blacklistedElements = const [], this.style, + this.navigationDelegateForIframe, }) : super(key: key); final String data; @@ -59,6 +61,11 @@ class Html extends StatelessWidget { /// Fancy New Parser parameters final Map style; + /// Decides how to handle a specific navigation request in the WebView of an + /// Iframe. It's necessary to use the webview_flutter package inside the app + /// to use NavigationDelegate. + final NavigationDelegate navigationDelegateForIframe; + @override Widget build(BuildContext context) { final double width = shrinkWrap ? null : MediaQuery.of(context).size.width; @@ -74,6 +81,7 @@ class Html extends StatelessWidget { style: style, customRender: customRender, blacklistedElements: blacklistedElements, + navigationDelegateForIframe: navigationDelegateForIframe, ), ); } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index a34d6571d3..f08a5c325a 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -11,6 +11,7 @@ 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:webview_flutter/webview_flutter.dart'; typedef OnTap = void Function(String url); typedef CustomRender = Widget Function( @@ -30,6 +31,7 @@ class HtmlParser extends StatelessWidget { final Map style; final Map customRender; final List blacklistedElements; + final NavigationDelegate navigationDelegateForIframe; HtmlParser({ @required this.htmlData, @@ -40,6 +42,7 @@ class HtmlParser extends StatelessWidget { this.style, this.customRender, this.blacklistedElements, + this.navigationDelegateForIframe, }); @override @@ -49,6 +52,7 @@ class HtmlParser extends StatelessWidget { document, customRender?.keys?.toList() ?? [], blacklistedElements, + navigationDelegateForIframe, ); StyledElement styledTree = applyCSS(lexedTree); StyledElement inlineStyledTree = applyInlineStyles(styledTree); @@ -69,7 +73,7 @@ class HtmlParser extends StatelessWidget { // scaling is used, but relies on https://github.com/flutter/flutter/pull/59711 // to wrap everything when larger accessibility fonts are used. return StyledText( - textSpan: parsedTree, + textSpan: parsedTree, style: cleanedTree.style, textScaleFactor: MediaQuery.of(context).textScaleFactor, ); @@ -90,6 +94,7 @@ class HtmlParser extends StatelessWidget { dom.Document html, List customRenderTags, List blacklistedElements, + NavigationDelegate navigationDelegateForIframe, ) { StyledElement tree = StyledElement( name: "[Tree Root]", @@ -98,8 +103,12 @@ class HtmlParser extends StatelessWidget { ); html.nodes.forEach((node) { - tree.children - .add(_recursiveLexer(node, customRenderTags, blacklistedElements)); + tree.children.add(_recursiveLexer( + node, + customRenderTags, + blacklistedElements, + navigationDelegateForIframe, + )); }); return tree; @@ -113,12 +122,17 @@ class HtmlParser extends StatelessWidget { dom.Node node, List customRenderTags, List blacklistedElements, + NavigationDelegate navigationDelegateForIframe, ) { List children = List(); node.nodes.forEach((childNode) { - children.add( - _recursiveLexer(childNode, customRenderTags, blacklistedElements)); + children.add(_recursiveLexer( + childNode, + customRenderTags, + blacklistedElements, + navigationDelegateForIframe, + )); }); //TODO(Sub6Resources): There's probably a more efficient way to look this up. @@ -131,7 +145,7 @@ class HtmlParser extends StatelessWidget { } else if (INTERACTABLE_ELEMENTS.contains(node.localName)) { return parseInteractableElement(node, children); } else if (REPLACED_ELEMENTS.contains(node.localName)) { - return parseReplacedElement(node); + return parseReplacedElement(node, navigationDelegateForIframe); } else if (LAYOUT_ELEMENTS.contains(node.localName)) { return parseLayoutElement(node, children); } else if (TABLE_STYLE_ELEMENTS.contains(node.localName)) { @@ -268,7 +282,8 @@ class HtmlParser extends StatelessWidget { shrinkWrap: context.parser.shrinkWrap, child: Stack( children: [ - if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE || tree.style?.listStylePosition == null) + if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE || + tree.style?.listStylePosition == null) PositionedDirectional( width: 30, //TODO derive this from list padding. start: 0, diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 5f1b6dde83..5516394676 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -5,16 +5,16 @@ import 'package:chewie/chewie.dart'; import 'package:chewie_audio/chewie_audio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter_html/src/utils.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:video_player/video_player.dart'; -import 'package:webview_flutter/webview_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; +import 'package:flutter_html/src/utils.dart'; import 'package:flutter_html/style.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. /// @@ -158,6 +158,7 @@ class IframeContentElement extends ReplacedElement { final String src; final double width; final double height; + final NavigationDelegate navigationDelegate; IframeContentElement({ String name, @@ -166,6 +167,7 @@ class IframeContentElement extends ReplacedElement { this.width, this.height, dom.Element node, + this.navigationDelegate, }) : super(name: name, style: style, node: node); @override @@ -176,6 +178,7 @@ class IframeContentElement extends ReplacedElement { child: WebView( initialUrl: src, javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: navigationDelegate, gestureRecognizers: { Factory(() => PlatformViewVerticalGestureRecognizer()) }, @@ -350,7 +353,10 @@ class RubyElement extends ReplacedElement { } } -ReplacedElement parseReplacedElement(dom.Element element) { +ReplacedElement parseReplacedElement( + dom.Element element, + NavigationDelegate navigationDelegateForIframe, +) { switch (element.localName) { case "audio": final sources = [ @@ -377,6 +383,7 @@ ReplacedElement parseReplacedElement(dom.Element element) { src: element.attributes['src'], width: double.tryParse(element.attributes['width'] ?? ""), height: double.tryParse(element.attributes['height'] ?? ""), + navigationDelegate: navigationDelegateForIframe, ); case "img": return ImageContentElement( diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index 97af4b2593..953f956cb2 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_html/src/html_elements.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/html_parser.dart'; +import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/style.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_html/flutter_html.dart'; void main() { testWidgets("Check that default parser does not fail on empty data", @@ -29,10 +29,12 @@ void testNewParser() { test("lexDomTree works correctly", () { StyledElement tree = HtmlParser.lexDomTree( - HtmlParser.parseHTML( - "Hello! Hello, World!Hello, New World!"), - [], - []); + HtmlParser.parseHTML( + "Hello! Hello, World!Hello, New World!"), + [], + [], + null, + ); print(tree.toString()); }); @@ -41,36 +43,43 @@ void testNewParser() { HtmlParser.parseHTML( "Hello, World! This is a link"), [], - []); + [], + null); print(tree.toString()); }); test("ContentElements work correctly", () { StyledElement tree = HtmlParser.lexDomTree( - HtmlParser.parseHTML(""), - [], - []); + HtmlParser.parseHTML(""), + [], + [], + null, + ); print(tree.toString()); }); test("Nesting of elements works correctly", () { StyledElement tree = HtmlParser.lexDomTree( - HtmlParser.parseHTML( - "LinkHello, World! Bold and Italic"), - [], - []); + HtmlParser.parseHTML( + "LinkHello, World! Bold and Italic"), + [], + [], + null, + ); print(tree.toString()); }); test("Video Content Source Parser works correctly", () { - ReplacedElement videoContentElement = - parseReplacedElement(HtmlParser.parseHTML(""" + ReplacedElement videoContentElement = parseReplacedElement( + HtmlParser.parseHTML(""" Your browser does not support the video tag. - """).getElementsByTagName("video")[0]); + """).getElementsByTagName("video")[0], + null, + ); expect(videoContentElement, isA()); if (videoContentElement is VideoContentElement) { @@ -82,14 +91,16 @@ void testNewParser() { }); test("Audio Content Source Parser works correctly", () { - ReplacedElement audioContentElement = - parseReplacedElement(HtmlParser.parseHTML(""" + ReplacedElement audioContentElement = parseReplacedElement( + HtmlParser.parseHTML(""" Your browser does not support the audio tag. - """).getElementsByTagName("audio")[0]); + """).getElementsByTagName("audio")[0], + null, + ); expect(audioContentElement, isA()); if (audioContentElement is AudioContentElement) { expect(audioContentElement.showControls, equals(true),