Skip to content

Commit ac4db23

Browse files
authored
Merge pull request Sub6Resources#434 from DFelten/feature/support-for-iframe-navigation-delegate
Add option to use navigation delegate feature of WebViews for iframes
2 parents b1644fd + ddadc41 commit ac4db23

File tree

4 files changed

+73
-32
lines changed

4 files changed

+73
-32
lines changed

lib/flutter_html.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library flutter_html;
33
import 'package:flutter/material.dart';
44
import 'package:flutter_html/html_parser.dart';
55
import 'package:flutter_html/style.dart';
6+
import 'package:webview_flutter/webview_flutter.dart';
67

78
class Html extends StatelessWidget {
89
/// The `Html` widget takes HTML as input and displays a RichText
@@ -40,6 +41,7 @@ class Html extends StatelessWidget {
4041
this.onImageTap,
4142
this.blacklistedElements = const [],
4243
this.style,
44+
this.navigationDelegateForIframe,
4345
}) : super(key: key);
4446

4547
final String data;
@@ -59,6 +61,11 @@ class Html extends StatelessWidget {
5961
/// Fancy New Parser parameters
6062
final Map<String, Style> style;
6163

64+
/// Decides how to handle a specific navigation request in the WebView of an
65+
/// Iframe. It's necessary to use the webview_flutter package inside the app
66+
/// to use NavigationDelegate.
67+
final NavigationDelegate navigationDelegateForIframe;
68+
6269
@override
6370
Widget build(BuildContext context) {
6471
final double width = shrinkWrap ? null : MediaQuery.of(context).size.width;
@@ -74,6 +81,7 @@ class Html extends StatelessWidget {
7481
style: style,
7582
customRender: customRender,
7683
blacklistedElements: blacklistedElements,
84+
navigationDelegateForIframe: navigationDelegateForIframe,
7785
),
7886
);
7987
}

lib/html_parser.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:flutter_html/src/utils.dart';
1111
import 'package:flutter_html/style.dart';
1212
import 'package:html/dom.dart' as dom;
1313
import 'package:html/parser.dart' as htmlparser;
14+
import 'package:webview_flutter/webview_flutter.dart';
1415

1516
typedef OnTap = void Function(String url);
1617
typedef CustomRender = Widget Function(
@@ -30,6 +31,7 @@ class HtmlParser extends StatelessWidget {
3031
final Map<String, Style> style;
3132
final Map<String, CustomRender> customRender;
3233
final List<String> blacklistedElements;
34+
final NavigationDelegate navigationDelegateForIframe;
3335

3436
HtmlParser({
3537
@required this.htmlData,
@@ -40,6 +42,7 @@ class HtmlParser extends StatelessWidget {
4042
this.style,
4143
this.customRender,
4244
this.blacklistedElements,
45+
this.navigationDelegateForIframe,
4346
});
4447

4548
@override
@@ -49,6 +52,7 @@ class HtmlParser extends StatelessWidget {
4952
document,
5053
customRender?.keys?.toList() ?? [],
5154
blacklistedElements,
55+
navigationDelegateForIframe,
5256
);
5357
StyledElement styledTree = applyCSS(lexedTree);
5458
StyledElement inlineStyledTree = applyInlineStyles(styledTree);
@@ -69,7 +73,7 @@ class HtmlParser extends StatelessWidget {
6973
// scaling is used, but relies on https://github.com/flutter/flutter/pull/59711
7074
// to wrap everything when larger accessibility fonts are used.
7175
return StyledText(
72-
textSpan: parsedTree,
76+
textSpan: parsedTree,
7377
style: cleanedTree.style,
7478
textScaleFactor: MediaQuery.of(context).textScaleFactor,
7579
);
@@ -90,6 +94,7 @@ class HtmlParser extends StatelessWidget {
9094
dom.Document html,
9195
List<String> customRenderTags,
9296
List<String> blacklistedElements,
97+
NavigationDelegate navigationDelegateForIframe,
9398
) {
9499
StyledElement tree = StyledElement(
95100
name: "[Tree Root]",
@@ -98,8 +103,12 @@ class HtmlParser extends StatelessWidget {
98103
);
99104

100105
html.nodes.forEach((node) {
101-
tree.children
102-
.add(_recursiveLexer(node, customRenderTags, blacklistedElements));
106+
tree.children.add(_recursiveLexer(
107+
node,
108+
customRenderTags,
109+
blacklistedElements,
110+
navigationDelegateForIframe,
111+
));
103112
});
104113

105114
return tree;
@@ -113,12 +122,17 @@ class HtmlParser extends StatelessWidget {
113122
dom.Node node,
114123
List<String> customRenderTags,
115124
List<String> blacklistedElements,
125+
NavigationDelegate navigationDelegateForIframe,
116126
) {
117127
List<StyledElement> children = List<StyledElement>();
118128

119129
node.nodes.forEach((childNode) {
120-
children.add(
121-
_recursiveLexer(childNode, customRenderTags, blacklistedElements));
130+
children.add(_recursiveLexer(
131+
childNode,
132+
customRenderTags,
133+
blacklistedElements,
134+
navigationDelegateForIframe,
135+
));
122136
});
123137

124138
//TODO(Sub6Resources): There's probably a more efficient way to look this up.
@@ -131,7 +145,7 @@ class HtmlParser extends StatelessWidget {
131145
} else if (INTERACTABLE_ELEMENTS.contains(node.localName)) {
132146
return parseInteractableElement(node, children);
133147
} else if (REPLACED_ELEMENTS.contains(node.localName)) {
134-
return parseReplacedElement(node);
148+
return parseReplacedElement(node, navigationDelegateForIframe);
135149
} else if (LAYOUT_ELEMENTS.contains(node.localName)) {
136150
return parseLayoutElement(node, children);
137151
} else if (TABLE_STYLE_ELEMENTS.contains(node.localName)) {
@@ -268,7 +282,8 @@ class HtmlParser extends StatelessWidget {
268282
shrinkWrap: context.parser.shrinkWrap,
269283
child: Stack(
270284
children: [
271-
if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE || tree.style?.listStylePosition == null)
285+
if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE ||
286+
tree.style?.listStylePosition == null)
272287
PositionedDirectional(
273288
width: 30, //TODO derive this from list padding.
274289
start: 0,

lib/src/replaced_element.dart

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import 'package:chewie/chewie.dart';
55
import 'package:chewie_audio/chewie_audio.dart';
66
import 'package:flutter/foundation.dart';
77
import 'package:flutter/gestures.dart';
8-
import 'package:flutter_html/src/utils.dart';
9-
import 'package:flutter_svg/flutter_svg.dart';
10-
import 'package:video_player/video_player.dart';
11-
import 'package:webview_flutter/webview_flutter.dart';
128
import 'package:flutter/material.dart';
139
import 'package:flutter/widgets.dart';
1410
import 'package:flutter_html/html_parser.dart';
1511
import 'package:flutter_html/src/html_elements.dart';
12+
import 'package:flutter_html/src/utils.dart';
1613
import 'package:flutter_html/style.dart';
14+
import 'package:flutter_svg/flutter_svg.dart';
1715
import 'package:html/dom.dart' as dom;
16+
import 'package:video_player/video_player.dart';
17+
import 'package:webview_flutter/webview_flutter.dart';
1818

1919
/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered.
2020
///
@@ -158,6 +158,7 @@ class IframeContentElement extends ReplacedElement {
158158
final String src;
159159
final double width;
160160
final double height;
161+
final NavigationDelegate navigationDelegate;
161162

162163
IframeContentElement({
163164
String name,
@@ -166,6 +167,7 @@ class IframeContentElement extends ReplacedElement {
166167
this.width,
167168
this.height,
168169
dom.Element node,
170+
this.navigationDelegate,
169171
}) : super(name: name, style: style, node: node);
170172

171173
@override
@@ -176,6 +178,7 @@ class IframeContentElement extends ReplacedElement {
176178
child: WebView(
177179
initialUrl: src,
178180
javascriptMode: JavascriptMode.unrestricted,
181+
navigationDelegate: navigationDelegate,
179182
gestureRecognizers: {
180183
Factory(() => PlatformViewVerticalGestureRecognizer())
181184
},
@@ -350,7 +353,10 @@ class RubyElement extends ReplacedElement {
350353
}
351354
}
352355

353-
ReplacedElement parseReplacedElement(dom.Element element) {
356+
ReplacedElement parseReplacedElement(
357+
dom.Element element,
358+
NavigationDelegate navigationDelegateForIframe,
359+
) {
354360
switch (element.localName) {
355361
case "audio":
356362
final sources = <String>[
@@ -377,6 +383,7 @@ ReplacedElement parseReplacedElement(dom.Element element) {
377383
src: element.attributes['src'],
378384
width: double.tryParse(element.attributes['width'] ?? ""),
379385
height: double.tryParse(element.attributes['height'] ?? ""),
386+
navigationDelegate: navigationDelegateForIframe,
380387
);
381388
case "img":
382389
return ImageContentElement(

test/html_parser_test.dart

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'package:flutter/material.dart';
2-
import 'package:flutter_html/src/html_elements.dart';
2+
import 'package:flutter_html/flutter_html.dart';
33
import 'package:flutter_html/html_parser.dart';
4+
import 'package:flutter_html/src/html_elements.dart';
45
import 'package:flutter_html/style.dart';
56
import 'package:flutter_test/flutter_test.dart';
6-
import 'package:flutter_html/flutter_html.dart';
77

88
void main() {
99
testWidgets("Check that default parser does not fail on empty data",
@@ -29,10 +29,12 @@ void testNewParser() {
2929

3030
test("lexDomTree works correctly", () {
3131
StyledElement tree = HtmlParser.lexDomTree(
32-
HtmlParser.parseHTML(
33-
"Hello! <b>Hello, World!</b><i>Hello, New World!</i>"),
34-
[],
35-
[]);
32+
HtmlParser.parseHTML(
33+
"Hello! <b>Hello, World!</b><i>Hello, New World!</i>"),
34+
[],
35+
[],
36+
null,
37+
);
3638
print(tree.toString());
3739
});
3840

@@ -41,36 +43,43 @@ void testNewParser() {
4143
HtmlParser.parseHTML(
4244
"Hello, World! <a href='https://example.com'>This is a link</a>"),
4345
[],
44-
[]);
46+
[],
47+
null);
4548
print(tree.toString());
4649
});
4750

4851
test("ContentElements work correctly", () {
4952
StyledElement tree = HtmlParser.lexDomTree(
50-
HtmlParser.parseHTML("<img src='https://image.example.com' />"),
51-
[],
52-
[]);
53+
HtmlParser.parseHTML("<img src='https://image.example.com' />"),
54+
[],
55+
[],
56+
null,
57+
);
5358
print(tree.toString());
5459
});
5560

5661
test("Nesting of elements works correctly", () {
5762
StyledElement tree = HtmlParser.lexDomTree(
58-
HtmlParser.parseHTML(
59-
"<div><div><div><div><a href='link'>Link</a><div>Hello, World! <b>Bold and <i>Italic</i></b></div></div></div></div></div>"),
60-
[],
61-
[]);
63+
HtmlParser.parseHTML(
64+
"<div><div><div><div><a href='link'>Link</a><div>Hello, World! <b>Bold and <i>Italic</i></b></div></div></div></div></div>"),
65+
[],
66+
[],
67+
null,
68+
);
6269
print(tree.toString());
6370
});
6471

6572
test("Video Content Source Parser works correctly", () {
66-
ReplacedElement videoContentElement =
67-
parseReplacedElement(HtmlParser.parseHTML("""
73+
ReplacedElement videoContentElement = parseReplacedElement(
74+
HtmlParser.parseHTML("""
6875
<video width="320" height="240" controls>
6976
<source src="movie.mp4" type="video/mp4">
7077
<source src="movie.ogg" type="video/ogg">
7178
Your browser does not support the video tag.
7279
</video>
73-
""").getElementsByTagName("video")[0]);
80+
""").getElementsByTagName("video")[0],
81+
null,
82+
);
7483

7584
expect(videoContentElement, isA<VideoContentElement>());
7685
if (videoContentElement is VideoContentElement) {
@@ -82,14 +91,16 @@ void testNewParser() {
8291
});
8392

8493
test("Audio Content Source Parser works correctly", () {
85-
ReplacedElement audioContentElement =
86-
parseReplacedElement(HtmlParser.parseHTML("""
94+
ReplacedElement audioContentElement = parseReplacedElement(
95+
HtmlParser.parseHTML("""
8796
<audio controls>
8897
<source src='audio.mp3' type='audio/mpeg'>
8998
<source src='audio.wav' type='audio/wav'>
9099
Your browser does not support the audio tag.
91100
</audio>
92-
""").getElementsByTagName("audio")[0]);
101+
""").getElementsByTagName("audio")[0],
102+
null,
103+
);
93104
expect(audioContentElement, isA<AudioContentElement>());
94105
if (audioContentElement is AudioContentElement) {
95106
expect(audioContentElement.showControls, equals(true),

0 commit comments

Comments
 (0)