Skip to content

Commit

Permalink
Merge pull request #632 from tneotia/feature/upgrade-custom-render
Browse files Browse the repository at this point in the history
Upgrade customRender to imitate customImageRender & add support for customRender for SelectableHtml
  • Loading branch information
erickok committed Dec 16, 2021
2 parents 6de34b9 + 742a20c commit b5aa467
Show file tree
Hide file tree
Showing 8 changed files with 552 additions and 327 deletions.
136 changes: 79 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,13 @@ Inner links (such as `<a href="#top">Back to the top</a>` will work out of the b

A powerful API that allows you to customize everything when rendering a specific HTML tag. This means you can change the default behaviour or add support for HTML elements that aren't supported natively. You can also make up your own custom tags in your HTML!

`customRender` accepts a `Map<String, CustomRender>`. The `CustomRender` type is a function that requires a `Widget` or `InlineSpan` to be returned. It exposes `RenderContext` and the `Widget` that would have been rendered by `Html` without a `customRender` defined. The `RenderContext` contains the build context, styling and the HTML element, with attrributes and its subtree,.
`customRender` accepts a `Map<CustomRenderMatcher, CustomRender>`.

To use this API, set the key as the tag of the HTML element you wish to provide a custom implementation for, and create a function with the above parameters that returns a `Widget` or `InlineSpan`.
`CustomRenderMatcher` is a function that requires a `bool` to be returned. It exposes the `RenderContext` which provides `BuildContext` and access to the HTML tree.

The `CustomRender` class has two constructors: `CustomRender.widget()` and `CustomRender.inlineSpan()`. Both require a `<Widget/InlineSpan> Function(RenderContext, Function())`. The `Function()` argument is a function that will provide you with the element's children when needed.

To use this API, create a matching function and an instance of `CustomRender`.

Note: If you add any custom tags, you must add these tags to the [`tagsList`](#tagslist) parameter, otherwise they will not be rendered. See below for an example.

Expand All @@ -286,21 +290,21 @@ Widget html = Html(
<flutter horizontal></flutter>
""",
customRender: {
"bird": (RenderContext context, Widget child) {
return TextSpan(text: "🐦");
},
"flutter": (RenderContext context, Widget child) {
return FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
);
},
birdMatcher(): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
flutterMatcher(): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
)),
},
tagsList: Html.tags..addAll(["bird", "flutter"]),
);
CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';
CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
```

2. Complex example - wrapping the default widget with your own, in this case placing a horizontal scroll around a (potentially too wide) table.
Expand All @@ -318,14 +322,16 @@ Widget html = Html(
</table>
""",
customRender: {
"table": (context, child) {
tableMatcher(): CustomRender.widget(widget: (context, child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: (context.tree as TableLayoutElement).toWidget(context),
);
}
}),
},
);
CustomRenderMatcher tableMatcher() => (context) => context.tree.element?.localName == "table" ?? false;
```

</details>
Expand All @@ -343,43 +349,52 @@ Widget html = Html(
<iframe src="https://www.youtube.com/embed/tgbNymZ7vqY"></iframe>
""",
customRender: {
"iframe": (RenderContext context, Widget child) {
final attrs = context.tree.element?.attributes;
if (attrs != null) {
double? width = double.tryParse(attrs['width'] ?? "");
double? height = double.tryParse(attrs['height'] ?? "");
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: attrs['src'] ?? "about:blank",
javascriptMode: JavascriptMode.unrestricted,
//no need for scrolling gesture recognizers on embedded youtube, so set gestureRecognizers null
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
gestureRecognizers: attrs['src'] != null && attrs['src']!.contains("youtube.com/embed") ? null : [
Factory(() => VerticalDragGestureRecognizer())
].toSet(),
navigationDelegate: (NavigationRequest request) async {
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
//on other iframe content allow all url loading
if (attrs['src'] != null && attrs['src']!.contains("youtube.com/embed")) {
if (!request.url.contains("youtube.com/embed")) {
return NavigationDecision.prevent;
} else {
return NavigationDecision.navigate;
}
} else {
return NavigationDecision.navigate;
}
},
),
);
} else {
return Container(height: 0);
}
}
}
iframeYT(): CustomRender.widget(widget: (context, buildChildren) {
double? width = double.tryParse(context.tree.attributes['width'] ?? "");
double? height = double.tryParse(context.tree.attributes['height'] ?? "");
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: context.tree.attributes['src']!,
javascriptMode: JavascriptMode.unrestricted,
navigationDelegate: (NavigationRequest request) async {
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
if (!request.url.contains("youtube.com/embed")) {
return NavigationDecision.prevent;
} else {
return NavigationDecision.navigate;
}
},
),
);
}),
iframeOther(): CustomRender.widget(widget: (context, buildChildren) {
double? width = double.tryParse(context.tree.attributes['width'] ?? "");
double? height = double.tryParse(context.tree.attributes['height'] ?? "");
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: context.tree.attributes['src'],
javascriptMode: JavascriptMode.unrestricted,
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
gestureRecognizers: [
Factory(() => VerticalDragGestureRecognizer())
].toSet(),
),
);
}),
iframeNull(): CustomRender.widget(widget: (context, buildChildren) => Container(height: 0, width: 0)),
}
);
CustomRenderMatcher iframeYT() => (context) => context.tree.element?.attributes['src']?.contains("youtube.com/embed") ?? false;
CustomRenderMatcher iframeOther() => (context) => !(context.tree.element?.attributes['src']?.contains("youtube.com/embed")
?? context.tree.element?.attributes['src'] == null);
CustomRenderMatcher iframeNull() => (context) => context.tree.element?.attributes['src'] == null;
```
</details>

Expand Down Expand Up @@ -804,16 +819,23 @@ Then, use the `customRender` parameter to add the widget to render Tex. It could
Widget htmlWidget = Html(
data: r"""<tex>i\hbar\frac{\partial}{\partial t}\Psi(\vec x,t) = -\frac{\hbar}{2m}\nabla^2\Psi(\vec x,t)+ V(\vec x)\Psi(\vec x,t)</tex>""",
customRender: {
"tex": (RenderContext context, _) => Math.tex(
context.tree.element!.text,
texMatcher(): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
context.tree.element?.innerHtml ?? '',
mathStyle: MathStyle.display,
textStyle: context.style.generateTextStyle(),
onErrorFallback: (FlutterMathException e) {
//return your error widget here e.g.
return Text(e.message);
if (context.parser.onMathError != null) {
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
} else {
return Text(e.message);
}
},
),
)),
},
tagsList: Html.tags..add('tex'),
);
CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';
```

### Table
Expand Down
48 changes: 27 additions & 21 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_math_fork/flutter_math.dart';

void main() => runApp(new MyApp());

Expand Down Expand Up @@ -250,7 +251,6 @@ class _MyHomePageState extends State<MyHomePage> {
body: SingleChildScrollView(
child: Html(
data: htmlData,
tagsList: Html.tags..addAll(["bird", "flutter"]),
style: {
"table": Style(
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
Expand All @@ -268,26 +268,32 @@ class _MyHomePageState extends State<MyHomePage> {
),
'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis),
},
customRender: {
"table": (context, child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child:
(context.tree as TableLayoutElement).toWidget(context),
);
},
"bird": (RenderContext context, Widget child) {
return TextSpan(text: "🐦");
},
"flutter": (RenderContext context, Widget child) {
return FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
);
},
tagsList: Html.tags..addAll(["tex", "bird", "flutter"]),
customRenders: {
tagMatcher("tex"): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
context.tree.element?.innerHtml ?? '',
mathStyle: MathStyle.display,
textStyle: context.style.generateTextStyle(),
onErrorFallback: (FlutterMathException e) {
if (context.parser.onMathError != null) {
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
} else {
return Text(e.message);
}
},
)),
tagMatcher("bird"): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
tagMatcher("flutter"): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
)),
tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: (context.tree as TableLayoutElement).toWidget(context),
)),
},
customImageRenders: {
networkSourceMatcher(domains: ["flutter.dev"]):
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: example
description: flutter_html example app.

publish_to: none
version: 1.0.0+1

environment:
Expand Down
Loading

0 comments on commit b5aa467

Please sign in to comment.