Skip to content

Commit 8d2e91b

Browse files
authored
Merge pull request #682 from tneotia/feature/selectable-text
Add support for selectable text via SelectableText.rich
2 parents baf9dd7 + fd26a16 commit 8d2e91b

File tree

4 files changed

+247
-18
lines changed

4 files changed

+247
-18
lines changed

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
3434
- [API Reference](#api-reference)
3535

3636
- [Constructors](#constructors)
37+
38+
- [Selectable Text](#selectable-text)
3739

3840
- [Parameters Table](#parameters)
3941

@@ -143,14 +145,30 @@ For a full example, see [here](https://github.com/Sub6Resources/flutter_html/tre
143145

144146
Below, you will find brief descriptions of the parameters the`Html` widget accepts and some code snippets to help you use this package.
145147

146-
## Constructors:
148+
### Constructors:
147149

148150
The package currently has two different constructors - `Html()` and `Html.fromDom()`.
149151

150152
The `Html()` constructor is for those who would like to directly pass HTML from the source to the package to be rendered.
151153

152154
If you would like to modify or sanitize the HTML before rendering it, then `Html.fromDom()` is for you - you can convert the HTML string to a `Document` and use its methods to modify the HTML as you wish. Then, you can directly pass the modified `Document` to the package. This eliminates the need to parse the modified `Document` back to a string, pass to `Html()`, and convert back to a `Document`, thus cutting down on load times.
153155

156+
#### Selectable Text
157+
158+
The package also has two constructors for selectable text support - `SelectableHtml()` and `SelectableHtml.fromDom()`.
159+
160+
The difference between the two is the same as noted above.
161+
162+
Please note: Due to Flutter [#38474](https://github.com/flutter/flutter/issues/38474), selectable text support is significantly watered down compared to the standard non-selectable version of the widget. The changes are as follows:
163+
164+
1. The list of tags that can be rendered is significantly reduced. Key omissions include no support for images/video/audio, table, and ul/ol.
165+
166+
2. No support for `customRender`, `customImageRender`, `onImageError`, `onImageTap`, `onMathError`, and `navigationDelegateForIframe`. (Support for `customRender` may be added in the future).
167+
168+
3. Styling support is significantly reduced. Only text-related styling works (e.g. bold or italic), while container related styling (e.g. borders or padding/margin) do not work.
169+
170+
Once the above issue is resolved, the aforementioned compromises will go away. Currently the `SelectableText.rich()` constructor does not support `WidgetSpan`s, resulting in the feature losses above.
171+
154172
### Parameters:
155173

156174
| Parameters | Description |
@@ -170,7 +188,9 @@ If you would like to modify or sanitize the HTML before rendering it, then `Html
170188

171189
### Getters:
172190

173-
Currently the only getter is `Html.tags`. This provides a list of all the tags the package renders. The main use case is to assist in blacklisting elements using `tagsList`. See an [example](#example-usage---tagslist---excluding-tags) below.
191+
1. `Html.tags`. This provides a list of all the tags the package renders. The main use case is to assist in excluding elements using `tagsList`. See an [example](#example-usage---tagslist---excluding-tags) below.
192+
193+
2. `SelectableHtml.tags`. This provides a list of all the tags that can be rendered in selectable mode.
174194

175195
### Data:
176196

@@ -419,7 +439,7 @@ A list of elements the `Html` widget should render. The list should contain the
419439
#### Example Usage - tagsList - Excluding Tags:
420440
You may have instances where you can choose between two different types of HTML tags to display the same content. In the example below, the `<video>` and `<iframe>` elements are going to display the same content.
421441

422-
The `blacklistedElements` parameter allows you to change which element is rendered. Iframes can be advantageous because they allow parallel loading - Flutter just has to wait for the webview to be initialized before rendering the page, possibly cutting down on load time. Video can be advantageous because it provides a 100% native experience with Flutter widgets, but it may take more time to render the page. You may know that Flutter webview is a little janky in its current state on Android, so using `blacklistedElements` and a simple condition, you can get the best of both worlds - choose the video widget to render on Android and the iframe webview to render on iOS.
442+
The `tagsList` parameter allows you to change which element is rendered. Iframes can be advantageous because they allow parallel loading - Flutter just has to wait for the webview to be initialized before rendering the page, possibly cutting down on load time. Video can be advantageous because it provides a 100% native experience with Flutter widgets, but it may take more time to render the page. You may know that Flutter webview is a little janky in its current state on Android, so using `tagsList` and a simple condition, you can get the best of both worlds - choose the video widget to render on Android and the iframe webview to render on iOS.
423443

424444
```dart
425445
Widget html = Html(

lib/flutter_html.dart

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,15 @@ import 'package:webview_flutter/webview_flutter.dart';
1111

1212
//export render context api
1313
export 'package:flutter_html/html_parser.dart';
14-
//export render context api
15-
export 'package:flutter_html/html_parser.dart';
16-
//export image render api
17-
export 'package:flutter_html/image_render.dart';
18-
//export image render api
1914
export 'package:flutter_html/image_render.dart';
15+
//export src for advanced custom render uses (e.g. casting context.tree)
2016
export 'package:flutter_html/src/anchor.dart';
21-
export 'package:flutter_html/src/anchor.dart';
22-
export 'package:flutter_html/src/interactable_element.dart';
2317
export 'package:flutter_html/src/interactable_element.dart';
24-
//export src for advanced custom render uses (e.g. casting context.tree)
25-
export 'package:flutter_html/src/layout_element.dart';
26-
//export src for advanced custom render uses (e.g. casting context.tree)
2718
export 'package:flutter_html/src/layout_element.dart';
2819
export 'package:flutter_html/src/replaced_element.dart';
29-
export 'package:flutter_html/src/replaced_element.dart';
30-
export 'package:flutter_html/src/styled_element.dart';
3120
export 'package:flutter_html/src/styled_element.dart';
3221
//export style api
3322
export 'package:flutter_html/style.dart';
34-
//export style api
35-
export 'package:flutter_html/style.dart';
3623

3724
class Html extends StatelessWidget {
3825
/// The `Html` widget takes HTML as input and displays a RichText
@@ -172,6 +159,7 @@ class Html extends StatelessWidget {
172159
onImageError: onImageError,
173160
onMathError: onMathError,
174161
shrinkWrap: shrinkWrap,
162+
selectable: false,
175163
style: style,
176164
customRender: customRender,
177165
imageRenders: {}
@@ -183,3 +171,105 @@ class Html extends StatelessWidget {
183171
);
184172
}
185173
}
174+
175+
class SelectableHtml extends StatelessWidget {
176+
/// The `SelectableHtml` widget takes HTML as input and displays a RichText
177+
/// tree of the parsed HTML content (which is selectable)
178+
///
179+
/// **Attributes**
180+
/// **data** *required* takes in a String of HTML data (required only for `Html` constructor).
181+
/// **document** *required* takes in a Document of HTML data (required only for `Html.fromDom` constructor).
182+
///
183+
/// **onLinkTap** This function is called whenever a link (`<a href>`)
184+
/// is tapped.
185+
///
186+
/// **tagsList** Tag names in this array will be the only tags rendered. By default all tags that support selectable content are rendered.
187+
///
188+
/// **style** Pass in the style information for the Html here.
189+
/// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/Style) for more info.
190+
///
191+
/// **PLEASE NOTE**
192+
///
193+
/// There are a few caveats due to Flutter [#38474](https://github.com/flutter/flutter/issues/38474):
194+
///
195+
/// 1. The list of tags that can be rendered is significantly reduced.
196+
/// Key omissions include no support for images/video/audio, table, and ul/ol because they all require widgets and `WidgetSpan`s.
197+
///
198+
/// 2. No support for `customRender`, `customImageRender`, `onImageError`, `onImageTap`, `onMathError`, and `navigationDelegateForIframe`.
199+
///
200+
/// 3. Styling support is significantly reduced. Only text-related styling works
201+
/// (e.g. bold or italic), while container related styling (e.g. borders or padding/margin)
202+
/// do not work because we can't use the `ContainerSpan` class (it needs an enclosing `WidgetSpan`).
203+
204+
SelectableHtml({
205+
Key? key,
206+
required this.data,
207+
this.onLinkTap,
208+
this.onCssParseError,
209+
this.shrinkWrap = false,
210+
this.style = const {},
211+
this.tagsList = const [],
212+
}) : document = null,
213+
super(key: key);
214+
215+
SelectableHtml.fromDom({
216+
Key? key,
217+
required this.document,
218+
this.onLinkTap,
219+
this.onCssParseError,
220+
this.shrinkWrap = false,
221+
this.style = const {},
222+
this.tagsList = const [],
223+
}) : data = null,
224+
super(key: key);
225+
226+
/// The HTML data passed to the widget as a String
227+
final String? data;
228+
229+
/// The HTML data passed to the widget as a pre-processed [dom.Document]
230+
final dom.Document? document;
231+
232+
/// A function that defines what to do when a link is tapped
233+
final OnTap? onLinkTap;
234+
235+
/// A function that defines what to do when CSS fails to parse
236+
final OnCssParseError? onCssParseError;
237+
238+
/// A parameter that should be set when the HTML widget is expected to be
239+
/// flexible
240+
final bool shrinkWrap;
241+
242+
/// A list of HTML tags that defines what elements are not rendered
243+
final List<String> tagsList;
244+
245+
/// An API that allows you to override the default style for any HTML element
246+
final Map<String, Style> style;
247+
248+
static List<String> get tags => new List<String>.from(SELECTABLE_ELEMENTS);
249+
250+
@override
251+
Widget build(BuildContext context) {
252+
final dom.Document doc = data != null ? HtmlParser.parseHTML(data!) : document!;
253+
final double? width = shrinkWrap ? null : MediaQuery.of(context).size.width;
254+
255+
return Container(
256+
width: width,
257+
child: HtmlParser(
258+
key: null,
259+
htmlData: doc,
260+
onLinkTap: onLinkTap,
261+
onImageTap: null,
262+
onCssParseError: onCssParseError,
263+
onImageError: null,
264+
onMathError: null,
265+
shrinkWrap: shrinkWrap,
266+
selectable: true,
267+
style: style,
268+
customRender: {},
269+
imageRenders: defaultImageRenders,
270+
tagsList: tagsList.isEmpty ? SelectableHtml.tags : tagsList,
271+
navigationDelegateForIframe: null,
272+
),
273+
);
274+
}
275+
}

lib/html_parser.dart

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class HtmlParser extends StatelessWidget {
4949
final ImageErrorListener? onImageError;
5050
final OnMathError? onMathError;
5151
final bool shrinkWrap;
52+
final bool selectable;
5253

5354
final Map<String, Style> style;
5455
final Map<String, CustomRender> customRender;
@@ -66,6 +67,7 @@ class HtmlParser extends StatelessWidget {
6667
required this.onImageError,
6768
required this.onMathError,
6869
required this.shrinkWrap,
70+
required this.selectable,
6971
required this.style,
7072
required this.customRender,
7173
required this.imageRenders,
@@ -104,6 +106,19 @@ class HtmlParser extends StatelessWidget {
104106
// using textScaleFactor = 1.0 (which is the default). This ensures the correct
105107
// scaling is used, but relies on https://github.com/flutter/flutter/pull/59711
106108
// to wrap everything when larger accessibility fonts are used.
109+
if (selectable) {
110+
return StyledText.selectable(
111+
textSpan: parsedTree as TextSpan,
112+
style: cleanedTree.style,
113+
textScaleFactor: MediaQuery.of(context).textScaleFactor,
114+
renderContext: RenderContext(
115+
buildContext: context,
116+
parser: this,
117+
tree: cleanedTree,
118+
style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
119+
),
120+
);
121+
}
107122
return StyledText(
108123
textSpan: parsedTree,
109124
style: cleanedTree.style,
@@ -321,6 +336,25 @@ class HtmlParser extends StatelessWidget {
321336

322337
//Return the correct InlineSpan based on the element type.
323338
if (tree.style.display == Display.BLOCK && tree.children.isNotEmpty) {
339+
if (newContext.parser.selectable) {
340+
return TextSpan(
341+
style: newContext.style.generateTextStyle(),
342+
children: tree.children
343+
.expandIndexed((i, childTree) => [
344+
if (childTree.style.display == Display.BLOCK &&
345+
i > 0 &&
346+
tree.children[i - 1] is ReplacedElement)
347+
TextSpan(text: "\n"),
348+
parseTree(newContext, childTree),
349+
if (i != tree.children.length - 1 &&
350+
childTree.style.display == Display.BLOCK &&
351+
childTree.element?.localName != "html" &&
352+
childTree.element?.localName != "body")
353+
TextSpan(text: "\n"),
354+
])
355+
.toList(),
356+
);
357+
}
324358
return WidgetSpan(
325359
child: ContainerSpan(
326360
key: AnchorKey.of(key, tree),
@@ -1001,17 +1035,39 @@ class StyledText extends StatelessWidget {
10011035
final double textScaleFactor;
10021036
final RenderContext renderContext;
10031037
final AnchorKey? key;
1038+
final bool _selectable;
10041039

10051040
const StyledText({
10061041
required this.textSpan,
10071042
required this.style,
10081043
this.textScaleFactor = 1.0,
10091044
required this.renderContext,
10101045
this.key,
1011-
}) : super(key: key);
1046+
}) : _selectable = false,
1047+
super(key: key);
1048+
1049+
const StyledText.selectable({
1050+
required TextSpan textSpan,
1051+
required this.style,
1052+
this.textScaleFactor = 1.0,
1053+
required this.renderContext,
1054+
this.key,
1055+
}) : textSpan = textSpan,
1056+
_selectable = true,
1057+
super(key: key);
10121058

10131059
@override
10141060
Widget build(BuildContext context) {
1061+
if (_selectable) {
1062+
return SelectableText.rich(
1063+
textSpan as TextSpan,
1064+
style: style.generateTextStyle(),
1065+
textAlign: style.textAlign,
1066+
textDirection: style.direction,
1067+
textScaleFactor: textScaleFactor,
1068+
maxLines: style.maxLines,
1069+
);
1070+
}
10151071
return SizedBox(
10161072
width: consumeExpandedBlock(style.display, renderContext),
10171073
child: Text.rich(

lib/src/html_elements.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,69 @@ const TABLE_CELL_ELEMENTS = ["th", "td"];
135135

136136
const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"];
137137

138+
const SELECTABLE_ELEMENTS = [
139+
"br",
140+
"a",
141+
"article",
142+
"aside",
143+
"blockquote",
144+
"body",
145+
"center",
146+
"dd",
147+
"div",
148+
"dl",
149+
"dt",
150+
"figcaption",
151+
"figure",
152+
"footer",
153+
"h1",
154+
"h2",
155+
"h3",
156+
"h4",
157+
"h5",
158+
"h6",
159+
"header",
160+
"hr",
161+
"html",
162+
"main",
163+
"nav",
164+
"noscript",
165+
"p",
166+
"pre",
167+
"section",
168+
"summary",
169+
"abbr",
170+
"acronym",
171+
"address",
172+
"b",
173+
"bdi",
174+
"bdo",
175+
"big",
176+
"cite",
177+
"code",
178+
"data",
179+
"del",
180+
"dfn",
181+
"em",
182+
"font",
183+
"i",
184+
"ins",
185+
"kbd",
186+
"mark",
187+
"q",
188+
"s",
189+
"samp",
190+
"small",
191+
"span",
192+
"strike",
193+
"strong",
194+
"time",
195+
"tt",
196+
"u",
197+
"var",
198+
"wbr",
199+
];
200+
138201
/**
139202
Here is a list of elements with planned support:
140203
a - i [x]

0 commit comments

Comments
 (0)