TF-292 Fix two scroll bars for medium sized screens

This commit is contained in:
dab246
2022-03-09 12:24:59 +07:00
committed by Dat H. Pham
parent 2972554be7
commit 6dcf9f9ea7
9 changed files with 353 additions and 91 deletions
+2
View File
@@ -45,6 +45,8 @@ export 'presentation/views/dialog/confirmation_dialog_builder.dart';
export 'presentation/views/dialog/edit_text_dialog_builder.dart';
export 'presentation/views/background/background_widget_builder.dart';
export 'presentation/views/html_viewer/html_content_viewer_widget.dart';
export 'presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
export 'presentation/views/html_viewer/html_viewer_controller_for_web.dart';
export 'presentation/views/floating_button/scrolling_floating_button_animated.dart';
export 'presentation/views/bottom_popup/cupertino_action_sheet_action_builder.dart';
export 'presentation/views/bottom_popup/cupertino_action_sheet_builder.dart';
@@ -0,0 +1,10 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// This file shims dart:ui in web-only scenarios, getting rid of the need to
/// suppress analyzer warnings.
// TODO(): flutter/flutter#55000 Remove this file once web-only dart:ui APIs
// are exposed from a dedicated place.
export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart';
@@ -0,0 +1,21 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Fake interface for the logic that this package needs from (web-only) dart:ui.
// This is conditionally exported so the analyzer sees these methods as available.
/// Shim for web_ui engine.PlatformViewRegistry
/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
// ignore: camel_case_types
class platformViewRegistry {
/// Shim for registerViewFactory
/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
static void registerViewFactory(
String viewTypeId, dynamic Function(int viewId) viewFactory) {}
}
/// Signature of callbacks that have no arguments and return no data.
typedef VoidCallback = void Function();
dynamic get window => null;
@@ -0,0 +1,5 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'dart:ui';
@@ -0,0 +1,232 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart';
import 'package:flutter/material.dart';
import 'dart:developer' as developer;
import 'package:universal_html/html.dart' as html;
import 'package:core/presentation/utils/shims/dart_ui.dart' as ui;
class HtmlContentViewerOnWeb extends StatefulWidget {
final String contentHtml;
final double widthContent;
final HtmlViewerControllerForWeb controller;
const HtmlContentViewerOnWeb({
Key? key,
required this.contentHtml,
required this.widthContent,
required this.controller,
}) : super(key: key);
@override
_HtmlContentViewerOnWebState createState() => _HtmlContentViewerOnWebState();
}
class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
/// The view ID for the IFrameElement. Must be unique.
late String createdViewId;
/// The actual height of the editor, used to automatically set the height
late double actualHeight;
Future<bool>? webInit;
String? _htmlData;
bool _isLoading = true;
int minHeight = 100;
@override
void initState() {
super.initState();
actualHeight = 400;
createdViewId = _getRandString(10);
widget.controller.viewId = createdViewId;
_setUpWeb();
}
String _getRandString(int len) {
var random = Random.secure();
var values = List<int>.generate(len, (i) => random.nextInt(255));
return base64UrlEncode(values);
}
String _generateHtmlDocument(String content) {
final htmlScripts = '''
<script type="text/javascript">
window.parent.addEventListener('message', handleMessage, false);
function handleMessage(e) {
if (e && e.data && e.data.includes("toIframe:")) {
var data = JSON.parse(e.data);
if (data["view"].includes("$createdViewId")) {
if (data["type"].includes("getHeight")) {
var height = document.body.scrollHeight;
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: htmlHeight", "height": height}), "*");
}
if (data["type"].includes("execCommand")) {
if (data["argument"] === null) {
document.execCommand(data["command"], false);
} else {
document.execCommand(data["command"], false, data["argument"]);
}
}
}
}
}
</script>
''';
final htmlTemplate = '''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
#editor {
outline: 0px solid transparent;
min-height: ${minHeight}px;
min-width: 300px;
color: #000000;
font-family: verdana;
}
table {
width: 100%;
max-width: 100%;
}
td {
padding: 13px;
margin: 0px;
}
th {
padding: 13px;
margin: 0px;
}
</style>
$htmlScripts
</head>
<body>
<div id="editor">$content</div>
</body>
</html>
''';
return htmlTemplate;
}
void _setUpWeb() {
_htmlData = _generateHtmlDocument(widget.contentHtml);
final iframe = html.IFrameElement()
..width = widget.widthContent.toString()
..height = actualHeight.toString()
..srcdoc = _htmlData ?? ''
..style.border = 'none'
..style.overflowY = 'hidden'
..onLoad.listen((event) async {
var data = <String, Object>{'type': 'toIframe: getHeight'};
data['view'] = createdViewId;
final jsonEncoder = JsonEncoder();
var jsonStr = jsonEncoder.convert(data);
html.window.postMessage(jsonStr, '*');
html.window.onBeforeUnload.listen((event) {
event.preventDefault();
});
html.window.onMessage.listen((event) {
var data = json.decode(event.data);
developer.log('onMessage(): data: $data', name: 'HtmlContentViewerOnWeb');
if (data['type'] != null &&
data['type'].contains('toDart: htmlHeight') &&
data['view'] == createdViewId) {
final docHeight = data['height'] ?? actualHeight;
developer.log('onMessage(): docHeight: $docHeight', name: 'HtmlContentViewerOnWeb');
if (docHeight != null && mounted) {
final scrollHeightWithBuffer = docHeight + 30.0;
if (scrollHeightWithBuffer > minHeight) {
setState(() {
actualHeight = scrollHeightWithBuffer;
_isLoading = false;
});
}
}
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
}
if (data['type'] != null &&
data['type'].contains('toDart: onChangeContent') &&
data['view'] == createdViewId) {
if (Scrollable.of(context) != null) {
Scrollable.of(context)!.position.ensureVisible(
context.findRenderObject()!,
duration: const Duration(milliseconds: 100),
curve: Curves.easeIn);
}
}
});
});
ui.platformViewRegistry.registerViewFactory(createdViewId, (int viewId) => iframe);
if (mounted) {
setState(() {
webInit = Future.value(true);
});
}
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
SizedBox(
height: actualHeight,
width: widget.widthContent,
child: _buildWebView(),
),
if (_isLoading) _buildLoadingView()
],
);
}
Widget _buildLoadingView() {
return Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(color: AppColor.colorTextButton)));
}
Widget _buildWebView() {
final htmlData = _htmlData;
if (htmlData == null || htmlData.isEmpty) {
return Container();
}
return Directionality(
textDirection: TextDirection.ltr,
child: FutureBuilder<bool>(
future: webInit,
builder: (context, snapshot) {
if (snapshot.hasData) {
return HtmlElementView(
key: ValueKey(htmlData),
viewType: createdViewId,
);
} else {
return Container();
}
}
)
);
}
}
@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:core/core.dart';
import 'package:easy_web_view/easy_web_view.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -12,9 +11,7 @@ import 'dart:developer' as developer;
class HtmlContentViewer extends StatefulWidget {
final String contentHtml;
final int minHeight;
final double widthContent;
final double? heightContent;
final Widget? loadingWidget;
/// Register this callback if you want a reference to the [WebViewController].
@@ -32,8 +29,6 @@ class HtmlContentViewer extends StatefulWidget {
Key? key,
required this.contentHtml,
required this.widthContent,
this.heightContent,
this.minHeight = 100,
this.loadingWidget,
this.onCreated,
this.urlLauncherDelegate,
@@ -48,9 +43,9 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
double? _webViewHeight = 1.0;
double? _webViewWidth = 1.0;
int minHeight = 100;
String? _htmlData;
late WebViewController _webViewController;
// late EasyWebViewControllerWrapperBase _easyWebViewControllerWrapperBase;
bool _isLoading = true;
@override
@@ -69,7 +64,7 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
<style>
#editor {
outline: 0px solid transparent;
min-height: ${widget.minHeight}px;
min-height: ${minHeight}px;
min-width: 300px;
color: #182952;
font-family: verdana;
@@ -106,28 +101,18 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
@override
Widget build(BuildContext context) {
if (kIsWeb) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
_buildWebViewOnBrowser(),
if (_isLoading) _buildLoadingView()
],
);
} else {
_webViewWidth = widget.widthContent;
return Stack(
alignment: AlignmentDirectional.center,
children: [
SizedBox(
height: _webViewHeight,
width: _webViewWidth,
child: _buildWebView(),
),
if (_isLoading) _buildLoadingView()
],
);
}
_webViewWidth = widget.widthContent;
return Stack(
alignment: AlignmentDirectional.center,
children: [
SizedBox(
height: _webViewHeight,
width: _webViewWidth,
child: _buildWebView(),
),
if (_isLoading) _buildLoadingView()
],
);
}
Widget _buildLoadingView() {
@@ -143,28 +128,6 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
}
}
Widget _buildWebViewOnBrowser() {
final htmlData = _htmlData;
if (htmlData == null || htmlData.isEmpty) {
return Container();
}
return EasyWebView(
key: ValueKey(htmlData),
src: htmlData,
onLoaded: (controller) async {
// _easyWebViewControllerWrapperBase = controller;
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
},
isHtml: true,
height: widget.heightContent,
webNavigationDelegate: _onNavigationOnWebBrowser,
);
}
Widget _buildWebView() {
final htmlData = _htmlData;
if (htmlData == null || htmlData.isEmpty) {
@@ -176,7 +139,6 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
backgroundColor: Colors.white,
onWebViewCreated: (controller) async {
_webViewController = controller;
developer.log('onWebViewCreated(): html: $htmlData', name: 'HtmlContentViewer');
await controller.loadHtmlString(htmlData, baseUrl: null);
widget.onCreated?.call(controller);
},
@@ -184,13 +146,9 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
final scrollHeightText = await _webViewController.runJavascriptReturningResult('document.body.scrollHeight');
final scrollHeight = double.tryParse(scrollHeightText);
developer.log('onPageFinished(): scrollHeightText: $scrollHeightText', name: 'HtmlContentViewer');
final scrollWidthText = await _webViewController.runJavascriptReturningResult('document.body.scrollWidth');
// var scrollWidth = double.tryParse(scrollWidthText);
developer.log('onPageFinished(): scrollWidthText: $scrollWidthText', name: 'HtmlContentViewer');
if ((scrollHeight != null) && mounted) {
final scrollHeightWithBuffer = scrollHeight + 30.0;
if (scrollHeightWithBuffer > widget.minHeight) {
if (scrollHeightWithBuffer > minHeight) {
setState(() {
_webViewHeight = scrollHeightWithBuffer;
_isLoading = false;
@@ -211,8 +169,6 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
}
FutureOr<NavigationDecision> _onNavigation(NavigationRequest navigation) async {
developer.log('_onNavigation()', name: 'HtmlContentViewer');
// for iOS / WKWebView necessary:
if (navigation.isForMainFrame && navigation.url == 'about:blank') {
return NavigationDecision.navigate;
}
@@ -233,24 +189,4 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
await launcher.launch(url);
return NavigationDecision.prevent;
}
FutureOr<WebNavigationDecision> _onNavigationOnWebBrowser(WebNavigationRequest navigation) async {
developer.log('_onNavigationOnWebBrowser()', name: 'HtmlContentViewer');
final requestUri = Uri.parse(navigation.url);
final mailtoHandler = widget.mailtoDelegate;
if (mailtoHandler != null && requestUri.isScheme('mailto')) {
await mailtoHandler(requestUri);
return WebNavigationDecision.prevent;
}
final url = navigation.url;
final urlDelegate = widget.urlLauncherDelegate;
if (urlDelegate != null) {
final handled = await urlDelegate(url);
if (handled) {
return WebNavigationDecision.prevent;
}
}
await launcher.launch(url);
return WebNavigationDecision.prevent;
}
}
@@ -0,0 +1,54 @@
import 'dart:convert';
import 'package:universal_html/html.dart' as html;
import 'package:flutter/foundation.dart';
class HtmlViewerControllerForWeb {
HtmlViewerControllerForWeb();
/// Manages the view ID for the [HtmlViewerControllerForWeb] on web
String? _viewId;
set viewId(String? viewId) => _viewId = viewId;
/// Recalculates the height of the editor to remove any vertical scrolling.
void recalculateHeight() {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: getHeight',
});
}
/// A function to quickly call a document.execCommand function in a readable format
void execCommand(String command, {String? argument}) {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: execCommand',
'command': command,
'argument': argument
});
}
/// A function to execute JS passed as a [WebScript] to the editor. This should
/// only be used on Flutter Web.
Future<dynamic> evaluateJavascriptWeb(String name,
{bool hasReturnValue = false}) async {
_evaluateJavascriptWeb(data: {'type': 'toIframe: $name'});
if (hasReturnValue) {
var e = await html.window.onMessage.firstWhere(
(element) => json.decode(element.data)['type'] == 'toDart: $name');
return json.decode(e.data);
}
}
/// Helper function to run javascript and check current environment
void _evaluateJavascriptWeb({required Map<String, Object?> data}) async {
if (kIsWeb) {
data['view'] = _viewId;
final jsonEncoder = JsonEncoder();
var json = jsonEncoder.convert(data);
html.window.postMessage(json, '*');
} else {
throw Exception('Non-Flutter Web environment detected');
}
}
}
+2 -6
View File
@@ -64,12 +64,6 @@ dependencies:
# webview_flutter
webview_flutter: 3.0.0
# easy_web_view: Display html for web browser
easy_web_view:
git:
url: https://github.com/dab246/easy_web_view.git
ref: easy_web_improvements
# url_launcher
url_launcher: 6.0.17
@@ -78,6 +72,8 @@ dependencies:
universal_html: 2.0.8
http: 0.13.4
dev_dependencies:
flutter_test:
sdk: flutter
@@ -1,5 +1,6 @@
import 'package:core/core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:model/model.dart';
@@ -32,12 +33,17 @@ class EmailContentItemBuilder {
Widget _buildItem() {
switch(_emailContent.type) {
case EmailContentType.textHtml:
return HtmlContentViewer(
widthContent: MediaQuery.of(_context).size.width,
heightContent: MediaQuery.of(_context).size.height,
contentHtml: _emailContent.content,
loadingWidget: loadingWidget,
mailtoDelegate: (uri) async {});
if (kIsWeb) {
return HtmlContentViewerOnWeb(
widthContent: MediaQuery.of(_context).size.width,
contentHtml: _emailContent.content,
controller: HtmlViewerControllerForWeb());
} else {
return HtmlContentViewer(
widthContent: MediaQuery.of(_context).size.width,
contentHtml: _emailContent.content,
loadingWidget: loadingWidget);
}
case EmailContentType.textPlain:
return Padding(
padding: EdgeInsets.zero,