Add lazy loading for html tag use background-image

Signed-off-by: dab246 <tdvu@linagora.com>
(cherry picked from commit 7f1a3b22ee8973c6756534a2c7da3cc1964ea9de)
This commit is contained in:
dab246
2023-10-16 18:45:27 +07:00
committed by Dat H. Pham
parent 72ba6d4145
commit 967015d0df
6 changed files with 130 additions and 16 deletions
@@ -1,5 +1,7 @@
import 'package:core/data/network/dio_client.dart'; import 'package:core/data/network/dio_client.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dartz/dartz.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
/// Transforms the HTML DOM. /// Transforms the HTML DOM.
@@ -22,4 +24,26 @@ abstract class DomTransformer {
document.children.insert(0, Element.html('<head></head>')); document.children.insert(0, Element.html('<head></head>'));
} }
} }
Tuple2<String, String>? findImageUrlFromStyleTag(String style) {
try {
final regExp = RegExp(r'background-image:\s*url\(([^)]+)\).*?');
final match = regExp.firstMatch(style);
if (match == null) {
return null;
}
final backgroundImageUrl = match.group(0) ?? '';
final imageUrl = match.group(1)?.replaceAll('\'', '').replaceAll('"', '') ?? '';
log('DomTransformer::findImageUrlFromStyleTag:backgroundImageUrl: $backgroundImageUrl | imageUrl: $imageUrl');
if (backgroundImageUrl.isNotEmpty && imageUrl.isNotEmpty) {
return Tuple2(backgroundImageUrl, imageUrl);
} else {
return null;
}
} catch (e) {
logError('DomTransformer::findImageUrlFromStyleTag:Exception: $e');
return null;
}
}
} }
@@ -0,0 +1,29 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/utils/app_logger.dart';
import 'package:html/dom.dart';
class AddLazyLoadingForBackgroundImageTransformer extends DomTransformer {
const AddLazyLoadingForBackgroundImageTransformer();
@override
Future<void> process({
required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID,
}) async {
final elements = document.querySelectorAll('[style*="background-image"]');
log('AddLazyLoadingForBackgroundImageTagTransformer::process:elements: ${elements.length}');
await Future.wait(elements.map((element) async {
var exStyle = element.attributes['style'];
final imageUrls = findImageUrlFromStyleTag(exStyle!);
if (imageUrls != null) {
exStyle = exStyle.replaceFirst(imageUrls.value1, '');
element.attributes['style'] = exStyle;
element.attributes['data-src'] = imageUrls.value2;
element.attributes.addAll({'lazy': ''});
log('AddLazyLoadingForBackgroundImageTagTransformer::process:NEW_ELEMENT: ${element.outerHtml}');
}
}));
}
}
@@ -72,6 +72,29 @@ class HtmlUtils {
</script> </script>
'''; ''';
static const scriptsHandleLazyLoadingBackgroundImage = '''
<script>
const lazyImages = document.querySelectorAll('[lazy]');
const lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target;
const src = lazyImage.dataset.src;
lazyImage.tagName.toLowerCase() === 'img'
? lazyImage.src = src
: lazyImage.style.backgroundImage = "url(\'" + src + "\')";
lazyImage.removeAttribute('lazy');
observer.unobserve(lazyImage);
}
});
});
lazyImages.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage);
});
</script>
''';
static String customCssStyleHtmlEditor({TextDirection direction = TextDirection.ltr}) { static String customCssStyleHtmlEditor({TextDirection direction = TextDirection.ltr}) {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
return ''' return '''
@@ -2,6 +2,7 @@
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/add_lazy_loading_for_background_image_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart';
@@ -48,6 +49,7 @@ class TransformConfiguration {
const AddTargetBlankInTagATransformer(), const AddTargetBlankInTagATransformer(),
const ImageTransformer(), const ImageTransformer(),
const AddTooltipLinkTransformer(), const AddTooltipLinkTransformer(),
const AddLazyLoadingForBackgroundImageTransformer(),
] ]
); );
@@ -86,6 +88,7 @@ class TransformConfiguration {
BlockCodeTransformer(), BlockCodeTransformer(),
AddTargetBlankInTagATransformer(), AddTargetBlankInTagATransformer(),
ImageTransformer(), ImageTransformer(),
AddLazyLoadingForBackgroundImageTransformer(),
]; ];
static const List<TextTransformer> standardTextTransformers = [ static const List<TextTransformer> standardTextTransformers = [
@@ -47,6 +47,7 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
late InAppWebViewController _webViewController; late InAppWebViewController _webViewController;
late double _actualHeight; late double _actualHeight;
late Set<Factory<OneSequenceGestureRecognizer>> _gestureRecognizers; late Set<Factory<OneSequenceGestureRecognizer>> _gestureRecognizers;
late String _customScripts;
final _loadingBarNotifier = ValueNotifier(true); final _loadingBarNotifier = ValueNotifier(true);
@@ -60,7 +61,6 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_actualHeight = _minHeight;
if (PlatformInfo.isAndroid) { if (PlatformInfo.isAndroid) {
_gestureRecognizers = { _gestureRecognizers = {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()), Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
@@ -71,13 +71,12 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()), Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
}; };
} }
_htmlData = generateHtml( if (PlatformInfo.isAndroid) {
widget.contentHtml, _customScripts = HtmlUtils.scriptsHandleLazyLoadingBackgroundImage + HtmlUtils.scriptsHandleContentSizeChanged;
direction: widget.direction, } else {
javaScripts: PlatformInfo.isAndroid _customScripts = HtmlUtils.scriptsHandleLazyLoadingBackgroundImage;
? HtmlUtils.scriptsHandleContentSizeChanged }
: null _initialData();
);
} }
@override @override
@@ -86,17 +85,19 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
log('_HtmlContentViewState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}'); log('_HtmlContentViewState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}');
if (widget.contentHtml != oldWidget.contentHtml || if (widget.contentHtml != oldWidget.contentHtml ||
widget.direction != oldWidget.direction) { widget.direction != oldWidget.direction) {
_actualHeight = _minHeight; _initialData();
_htmlData = generateHtml(
widget.contentHtml,
direction: widget.direction,
javaScripts: PlatformInfo.isAndroid
? HtmlUtils.scriptsHandleContentSizeChanged
: null
);
} }
} }
void _initialData() {
_actualHeight = _minHeight;
_htmlData = generateHtml(
widget.contentHtml,
direction: widget.direction,
javaScripts: _customScripts
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack(children: [ return Stack(children: [
+34
View File
@@ -0,0 +1,34 @@
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const imageTransformer = ImageTransformer();
group('findImageUrlFromStyleTag test', () {
test('Test findImageUrlFromStyleTag with valid input', () {
const style = 'background-image: url(\'example.com/image.jpg\');';
final result = imageTransformer.findImageUrlFromStyleTag(style);
expect(result!.value1, 'background-image: url(\'example.com/image.jpg\')');
expect(result.value2, 'example.com/image.jpg');
});
test('Test findImageUrlFromStyleTag with valid input and no quotation marks', () {
const style = 'background-image: url(example.com/image.jpg);';
final result = imageTransformer.findImageUrlFromStyleTag(style);
expect(result!.value1, 'background-image: url(example.com/image.jpg)');
expect(result.value2, 'example.com/image.jpg');
});
test('Test findImageUrlFromStyleTag with empty input', () {
const style = '';
final result = imageTransformer.findImageUrlFromStyleTag(style);
expect(result, null);
});
test('Test findImageUrlFromStyleTag with invalid input', () {
const style = 'background-image: invalid-url(\'example.com/image.jpg\');';
final result = imageTransformer.findImageUrlFromStyleTag(style);
expect(result, null);
});
});
}