Sonnat Project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

326 lines
11 KiB

  1. library flutter_html;
  2. import 'package:flutter/material.dart';
  3. import 'package:html/dom.dart' as dom;
  4. import 'package:sonnat/core/html/custom_render.dart';
  5. import 'package:sonnat/core/html/html_parser.dart';
  6. import 'package:sonnat/core/html/src/html_elements.dart';
  7. import 'package:sonnat/core/html/style.dart';
  8. class Html extends StatefulWidget {
  9. Html({
  10. super.key,
  11. GlobalKey? anchorKey,
  12. required this.data,
  13. this.onLinkTap,
  14. this.onAnchorTap,
  15. this.customRenders = const {},
  16. this.onCssParseError,
  17. this.onImageError,
  18. this.shrinkWrap = false,
  19. this.onImageTap,
  20. this.tagsList = const [],
  21. this.style = const {},
  22. }) : documentElement = null,
  23. assert(data != null),
  24. _anchorKey = anchorKey ?? GlobalKey();
  25. Html.fromDom({
  26. super.key,
  27. GlobalKey? anchorKey,
  28. @required dom.Document? document,
  29. this.onLinkTap,
  30. this.onAnchorTap,
  31. this.customRenders = const {},
  32. this.onCssParseError,
  33. this.onImageError,
  34. this.shrinkWrap = false,
  35. this.onImageTap,
  36. this.tagsList = const [],
  37. this.style = const {},
  38. }) : data = null,
  39. assert(document != null),
  40. documentElement = document!.documentElement,
  41. _anchorKey = anchorKey ?? GlobalKey();
  42. Html.fromElement({
  43. super.key,
  44. GlobalKey? anchorKey,
  45. @required this.documentElement,
  46. this.onLinkTap,
  47. this.onAnchorTap,
  48. this.customRenders = const {},
  49. this.onCssParseError,
  50. this.onImageError,
  51. this.shrinkWrap = false,
  52. this.onImageTap,
  53. this.tagsList = const [],
  54. this.style = const {},
  55. }) : data = null,
  56. assert(documentElement != null),
  57. _anchorKey = anchorKey ?? GlobalKey();
  58. /// A unique key for this Html widget to ensure uniqueness of anchors
  59. final GlobalKey _anchorKey;
  60. /// The HTML data passed to the widget as a String
  61. final String? data;
  62. /// The HTML data passed to the widget as a pre-processed [dom.Element]
  63. final dom.Element? documentElement;
  64. /// A function that defines what to do when a link is tapped
  65. final OnTap? onLinkTap;
  66. /// A function that defines what to do when an anchor link is tapped. When this value is set,
  67. /// the default anchor behaviour is overwritten.
  68. final OnTap? onAnchorTap;
  69. /// A function that defines what to do when CSS fails to parse
  70. final OnCssParseError? onCssParseError;
  71. /// A function that defines what to do when an image errors
  72. final ImageErrorListener? onImageError;
  73. /// A parameter that should be set when the HTML widget is expected to be
  74. /// flexible
  75. final bool shrinkWrap;
  76. /// A function that defines what to do when an image is tapped
  77. final OnTap? onImageTap;
  78. /// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered.
  79. final List<String> tagsList;
  80. /// Either return a custom widget for specific node types or return null to
  81. /// fallback to the default rendering.
  82. final Map<CustomRenderMatcher, CustomRender> customRenders;
  83. /// An API that allows you to override the default style for any HTML element
  84. final Map<String, Style> style;
  85. static List<String> get tags => List<String>.from(HtmlElements.styledElements)
  86. ..addAll(HtmlElements.interactableElements)
  87. ..addAll(HtmlElements.replacedElements)
  88. ..addAll(HtmlElements.layoutElements)
  89. ..addAll(HtmlElements.tableCellElements)
  90. ..addAll(HtmlElements.tableDefinitionElements)
  91. ..addAll(HtmlElements.externalElements);
  92. @override
  93. State<StatefulWidget> createState() => _HtmlState();
  94. }
  95. class _HtmlState extends State<Html> {
  96. late dom.Element documentElement;
  97. @override
  98. void initState() {
  99. super.initState();
  100. documentElement = widget.data != null
  101. ? HtmlParser.parseHTML(widget.data!)
  102. : widget.documentElement!;
  103. }
  104. @override
  105. void didUpdateWidget(Html oldWidget) {
  106. super.didUpdateWidget(oldWidget);
  107. if ((widget.data != null && oldWidget.data != widget.data) ||
  108. oldWidget.documentElement != widget.documentElement) {
  109. documentElement = widget.data != null
  110. ? HtmlParser.parseHTML(widget.data!)
  111. : widget.documentElement!;
  112. }
  113. }
  114. @override
  115. Widget build(BuildContext context) {
  116. return HtmlParser(
  117. key: widget._anchorKey,
  118. htmlData: documentElement,
  119. onLinkTap: widget.onLinkTap,
  120. onAnchorTap: widget.onAnchorTap,
  121. onImageTap: widget.onImageTap,
  122. onCssParseError: widget.onCssParseError,
  123. onImageError: widget.onImageError,
  124. shrinkWrap: widget.shrinkWrap,
  125. selectable: false,
  126. style: widget.style,
  127. customRenders: {}
  128. ..addAll(widget.customRenders)
  129. ..addAll(generateDefaultRenders()),
  130. tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList,
  131. );
  132. }
  133. }
  134. class SelectableHtml extends StatefulWidget {
  135. /// The `SelectableHtml` widget takes HTML as input and displays a RichText
  136. /// tree of the parsed HTML content (which is selectable)
  137. ///
  138. /// **Attributes**
  139. /// **data** *required* takes in a String of HTML data (required only for `Html` constructor).
  140. /// **documentElement** *required* takes in a Element of HTML data (required only for `Html.fromDom` and `Html.fromElement` constructor).
  141. ///
  142. /// **onLinkTap** This function is called whenever a link (`<a href>`)
  143. /// is tapped.
  144. ///
  145. /// **onAnchorTap** This function is called whenever an anchor (#anchor-id)
  146. /// is tapped.
  147. ///
  148. /// **tagsList** Tag names in this array will be the only tags rendered. By default, all tags that support selectable content are rendered.
  149. ///
  150. /// **style** Pass in the style information for the Html here.
  151. /// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/Style) for more info.
  152. ///
  153. /// **PLEASE NOTE**
  154. ///
  155. /// There are a few caveats due to Flutter [#38474](https://github.com/flutter/flutter/issues/38474):
  156. ///
  157. /// 1. The list of tags that can be rendered is significantly reduced.
  158. /// Key omissions include no support for images/video/audio, table, and ul/ol because they all require widgets and `WidgetSpan`s.
  159. ///
  160. /// 2. No support for `customRender`, `customImageRender`, `onImageError`, `onImageTap`, `onMathError`, and `navigationDelegateForIframe`.
  161. ///
  162. /// 3. Styling support is significantly reduced. Only text-related styling works
  163. /// (e.g. bold or italic), while container related styling (e.g. borders or padding/margin)
  164. /// do not work because we can't use the `ContainerSpan` class (it needs an enclosing `WidgetSpan`).
  165. SelectableHtml({
  166. super.key,
  167. GlobalKey? anchorKey,
  168. required this.data,
  169. this.onLinkTap,
  170. this.onAnchorTap,
  171. this.onCssParseError,
  172. this.shrinkWrap = false,
  173. this.style = const {},
  174. this.customRenders = const {},
  175. this.tagsList = const [],
  176. this.selectionControls,
  177. this.scrollPhysics,
  178. }) : documentElement = null,
  179. assert(data != null),
  180. _anchorKey = anchorKey ?? GlobalKey();
  181. SelectableHtml.fromDom({
  182. super.key,
  183. GlobalKey? anchorKey,
  184. @required dom.Document? document,
  185. this.onLinkTap,
  186. this.onAnchorTap,
  187. this.onCssParseError,
  188. this.shrinkWrap = false,
  189. this.style = const {},
  190. this.customRenders = const {},
  191. this.tagsList = const [],
  192. this.selectionControls,
  193. this.scrollPhysics,
  194. }) : data = null,
  195. assert(document != null),
  196. documentElement = document!.documentElement,
  197. _anchorKey = anchorKey ?? GlobalKey();
  198. SelectableHtml.fromElement({
  199. super.key,
  200. GlobalKey? anchorKey,
  201. @required this.documentElement,
  202. this.onLinkTap,
  203. this.onAnchorTap,
  204. this.onCssParseError,
  205. this.shrinkWrap = false,
  206. this.style = const {},
  207. this.customRenders = const {},
  208. this.tagsList = const [],
  209. this.selectionControls,
  210. this.scrollPhysics,
  211. }) : data = null,
  212. assert(documentElement != null),
  213. _anchorKey = anchorKey ?? GlobalKey();
  214. /// A unique key for this Html widget to ensure uniqueness of anchors
  215. final GlobalKey _anchorKey;
  216. /// The HTML data passed to the widget as a String
  217. final String? data;
  218. /// The HTML data passed to the widget as a pre-processed [dom.Element]
  219. final dom.Element? documentElement;
  220. /// A function that defines what to do when a link is tapped
  221. final OnTap? onLinkTap;
  222. /// A function that defines what to do when an anchor link is tapped. When this value is set,
  223. /// the default anchor behaviour is overwritten.
  224. final OnTap? onAnchorTap;
  225. /// A function that defines what to do when CSS fails to parse
  226. final OnCssParseError? onCssParseError;
  227. /// A parameter that should be set when the HTML widget is expected to be
  228. /// have a flexible width, that doesn't always fill its maximum width
  229. /// constraints. For example, auto horizontal margins are ignored, and
  230. /// block-level elements only take up the width they need.
  231. final bool shrinkWrap;
  232. /// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered.
  233. final List<String> tagsList;
  234. /// An API that allows you to override the default style for any HTML element
  235. final Map<String, Style> style;
  236. /// Custom Selection controls allows you to override default toolbar and build custom toolbar
  237. /// options
  238. final TextSelectionControls? selectionControls;
  239. /// Allows you to override the default scrollPhysics for [SelectableText.rich]
  240. final ScrollPhysics? scrollPhysics;
  241. /// Either return a custom widget for specific node types or return null to
  242. /// fallback to the default rendering.
  243. final Map<CustomRenderMatcher, SelectableCustomRender> customRenders;
  244. static List<String> get tags =>
  245. List<String>.from(HtmlElements.selectableElements);
  246. @override
  247. State<StatefulWidget> createState() => _SelectableHtmlState();
  248. }
  249. class _SelectableHtmlState extends State<SelectableHtml> {
  250. late final dom.Element documentElement;
  251. @override
  252. void initState() {
  253. super.initState();
  254. documentElement = widget.data != null
  255. ? HtmlParser.parseHTML(widget.data!)
  256. : widget.documentElement!;
  257. }
  258. @override
  259. Widget build(BuildContext context) {
  260. return SizedBox(
  261. width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
  262. child: HtmlParser(
  263. key: widget._anchorKey,
  264. htmlData: documentElement,
  265. onLinkTap: widget.onLinkTap,
  266. onAnchorTap: widget.onAnchorTap,
  267. onImageTap: null,
  268. onCssParseError: widget.onCssParseError,
  269. onImageError: null,
  270. shrinkWrap: widget.shrinkWrap,
  271. selectable: true,
  272. style: widget.style,
  273. customRenders: {}
  274. ..addAll(widget.customRenders)
  275. ..addAll(generateDefaultRenders()),
  276. tagsList:
  277. widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList,
  278. selectionControls: widget.selectionControls,
  279. scrollPhysics: widget.scrollPhysics,
  280. ),
  281. );
  282. }
  283. }