TF-292 Fix two scroll bars for medium sized screens
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user