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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+29
@@ -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: [
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user