Last active
January 30, 2024 03:26
-
-
Save ltvu93/9df3536c4eee22dc8bc8b7ffea614f7f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_html/flutter_html.dart'; | |
import 'package:html/dom.dart' as dom; | |
void main() { | |
runApp(TestApp()); | |
} | |
class TestApp extends StatelessWidget { | |
const TestApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
final htmlText = ''' | |
<h1>Free Online HTML Editor</h1> | |
<p>Feel free to use this <strong>free online HTML editor</strong> to generate HTML code for your own website, blog, email newsletter, etc.</p> | |
<p>To get started:</p> | |
<ul> | |
<li>Delete this content and enter your own content</li> | |
<li>To view the source code, simply select the <kbd>Tools > Source</kbd> menu option above</li> | |
<li>Rinse and repeat as required</li> | |
<li>Copy and paste the source code into your own website, blog, email newsletter, etc</li> | |
</ul> | |
<p>Go ahead - try it!</p> | |
<p>HTML code generated at <a href="https://htmleditor.online/">htmleditor.online</a>.</p> | |
'''; | |
return MaterialApp( | |
home: Scaffold( | |
body: Center( | |
child: Padding( | |
padding: const EdgeInsets.all(16), | |
child: ReadMoreHtml( | |
htmlText: htmlText, | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class ReadMoreHtml extends StatefulWidget { | |
final String htmlText; | |
final TextStyle? style; | |
final int maxTextLength; | |
final String showMoreText; | |
final TextStyle? showMoreStyle; | |
final VoidCallback? onShowMorePressed; | |
const ReadMoreHtml({ | |
Key? key, | |
required this.htmlText, | |
this.style, | |
this.maxTextLength = 120, | |
this.showMoreText = 'read more', | |
this.showMoreStyle, | |
this.onShowMorePressed, | |
}) : super(key: key); | |
@override | |
State<ReadMoreHtml> createState() => _ReadMoreHtmlState(); | |
} | |
class _ReadMoreHtmlState extends State<ReadMoreHtml> { | |
late dom.Element fullHtmlElement; | |
late dom.Element cutHtmlElement; | |
bool expanded = false; | |
static const String _kEllipsis = '\u2026'; | |
static const String _showMoreTag = 'showMore'; | |
@override | |
void initState() { | |
super.initState(); | |
_processData(); | |
} | |
@override | |
void didUpdateWidget(oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.htmlText != oldWidget.htmlText || widget.maxTextLength != oldWidget.maxTextLength) { | |
_processData(); | |
} | |
} | |
void _processData() { | |
fullHtmlElement = HtmlParser.parseHTML(widget.htmlText); | |
cutHtmlElement = fullHtmlElement.clone(true); | |
if (fullHtmlElement.text.length <= widget.maxTextLength) { | |
return; | |
} | |
curTextCount = 0; | |
cutElementText(cutHtmlElement, widget.maxTextLength); | |
addReadMoreElement(cutHtmlElement); | |
} | |
int curTextCount = 0; | |
void cutElementText(dom.Node curNode, int maxTextLength) { | |
final nodes = [...curNode.nodes]; | |
for (final node in nodes) { | |
if (node.nodeType == dom.Node.ELEMENT_NODE) { | |
if (curTextCount >= maxTextLength) { | |
node.remove(); | |
} else { | |
cutElementText(node, maxTextLength); | |
} | |
} | |
if (node.nodeType == dom.Node.TEXT_NODE) { | |
final newTextCount = curTextCount + node.text!.length; | |
if (newTextCount > maxTextLength) { | |
node.text = node.text!.substring(0, node.text!.length - (newTextCount - maxTextLength)); | |
} | |
curTextCount += node.text!.length; | |
} | |
} | |
} | |
void addReadMoreElement(dom.Element element) { | |
final lastNode = getLastNode(element); | |
final readMoreTagElement = dom.Element.tag(_showMoreTag); | |
final addNode = lastNode.nodeType == dom.Node.ELEMENT_NODE ? lastNode : lastNode.parent; | |
addNode!.append(readMoreTagElement); | |
} | |
dom.Node getLastNode(dom.Node node) { | |
if (node.nodes.isEmpty) return node; | |
return getLastNode(node.nodes.last); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final textStyle = widget.style ?? Theme.of(context).textTheme.bodyMedium!; | |
return Html.fromElement( | |
documentElement: expanded ? fullHtmlElement : cutHtmlElement, | |
style: { | |
'html': Style.fromTextStyle(textStyle), | |
'body': Style( | |
margin: Margins.zero, | |
padding: HtmlPaddings.zero, | |
), | |
'p': Style( | |
margin: Margins.zero, | |
padding: HtmlPaddings.zero, | |
), | |
}, | |
extensions: [ | |
TagExtension( | |
tagsToExtend: {_showMoreTag}, | |
builder: (extensionContext) { | |
return RichText( | |
text: TextSpan( | |
style: textStyle, | |
children: [ | |
TextSpan( | |
text: '$_kEllipsis ', | |
), | |
if (widget.showMoreText.isNotEmpty) ...[ | |
TextSpan( | |
text: widget.showMoreText, | |
style: widget.showMoreStyle ?? textStyle.copyWith(color: Colors.pink), | |
recognizer: TapGestureRecognizer()..onTap = onReadMorePressed, | |
), | |
], | |
], | |
), | |
); | |
}, | |
), | |
], | |
); | |
} | |
void onReadMorePressed() { | |
setState(() { | |
expanded = true; | |
}); | |
widget.onShowMorePressed?.call(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment