I'm a noob to Flutter Web. I have a package that I'm trying to create support for in Flutter Web, but it uses a webview for some functions. Webviews aren't supported in Flutter Web so I'm using a IFrameElement and ui.platformViewRegistry.registerViewFactory() to act like a webview. I'm passing an HTML String to be loaded rather than a file.
I need to be able to run JS functions and get data from JS. I've tried a lot of different things with events and event listeners, also context.callMethod() and none of it has worked so far. Is there a simple way to accomplish this?
For reference, I am using the Summernote library and I can run something like \$('#summernote').summernote('reset'); to reset the Summernote editor. Sometimes I need to get data from JS so I am running var str = \$('#summernote').summernote('code'); console.log(str); which gives me the HTML code in the editor.
Thanks in advance!
Code for reference:
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:html_editor_enhanced/utils/pick_image.dart';
import 'package:path/path.dart' as p;
import 'package:flutter/material.dart';
import 'dart:html' as html;
import 'dart:js' as js;
import 'dart:ui' as ui;
bool callbacksInitialized = false;
js.JsObject jsDocument;
class HtmlEditorWidgetWeb extends StatelessWidget {
HtmlEditorWidgetWeb({
Key key,
this.value,
this.height,
this.useBottomSheet,
this.imageWidth,
this.showBottomToolbar,
this.hint,
this.callbacks,
this.toolbar,
this.darkMode
}) : super(key: key);
final String value;
final double height;
final bool useBottomSheet;
final double imageWidth;
final bool showBottomToolbar;
final String hint;
final UniqueKey webViewKey = UniqueKey();
final Callbacks callbacks;
final List<Toolbar> toolbar;
final bool darkMode;
final String createdViewId = 'html_editor_web';
#override
Widget build(BuildContext context) {
String summernoteToolbar = "[\n";
for (Toolbar t in toolbar) {
summernoteToolbar = summernoteToolbar +
"['${t.getGroupName()}', ${t.getButtons()}],\n";
}
summernoteToolbar = summernoteToolbar + "],";
String darkCSS = "";
if ((Theme.of(context).brightness == Brightness.dark || darkMode == true) && darkMode != false) {
darkCSS = "<link href=\"packages/html_editor_enhanced/assets/summernote-lite-dark.css\" rel=\"stylesheet\">";
}
String htmlString = """
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="Flutter Summernote HTML Editor">
<meta name="author" content="xrb21">
<title>Summernote Text Editor HTML</title>
<script src="main.dart.js" type="application/javascript"></script>
<script src="app.js" defer></script>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/summernote#0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote#0.8.18/dist/summernote-lite.min.js"></script>
$darkCSS
<script>
function test() {
console.log("Listening");
}
</script>
</head>
<body>
<div id="summernote-2"></div>
<script type="text/javascript">
\$('#summernote-2').summernote({
placeholder: "$hint",
tabsize: 2,
height: ${height - 125},
maxHeight: ${height - 125},
toolbar: $summernoteToolbar
disableGrammar: false,
spellCheck: false
});
document.addEventListener("setFS", function(){
console.log('fired');
\$('#summernote-2').summernote("fullscreen.toggle");
});
</script>
<style>
body {
display: block;
margin: 0px;
}
.note-editor.note-airframe, .note-editor.note-frame {
border: 0px solid #a9a9a9;
}
.note-frame {
border-radius: 0px;
}
</style>
</body>
</html>
""";
html.window.onMessage.forEach((element) {
print('Event Received in callback: ${element.data}');
});
// todo use postmessage and concatenation to accomplish callbacks
final html.IFrameElement iframe = html.IFrameElement()
..width = MediaQuery.of(context).size.width.toString() //'800'
..height = MediaQuery.of(context).size.height.toString() //'400'
..srcdoc = htmlString
..style.border = 'none'
..onLoad.listen((event) async {
html.document.on['setFS'].listen((html.Event event) {
print("HEY! I'M LISTENING!");
});
html.document.dispatchEvent(html.Event("setFS"));
});
ui.platformViewRegistry.registerViewFactory(
createdViewId, (int viewId) => iframe);
return Column(
children: <Widget>[
Expanded(
child: Directionality(
textDirection: TextDirection.ltr,
child: HtmlElementView(
viewType: createdViewId,
)
)
),
],
);
}
}
A little bit hacky, but here's the solution I use:
Dart -> JS
In dart:
html.window.postMessage(//data to send here, "*");
and in the IframeElement HTML <script>:
window.parent.addEventListener('message', handleMessage, false);
function handleMessage(e) {
var data = e.data;
}
I personally use JSON when sending data to make it easier to send/receive/parse. So in Dart that is a Map<String, dynamic>, sent like this:
final data = Map<String, dynamic>{
//your data here
}
final jsonEncoder = JsonEncoder();
final json = jsonEncoder.convert(data);
html.window.postMessage(json, "*");
and in JS:
window.parent.addEventListener('message', handleMessage, false);
function handleMessage(e) {
var data = JSON.parse(e.data);
}
A suggestion would be to create a unique key/string that you can pass in between JS and Dart so you make sure you are intercepting the correct postMessage every time.
JS -> Dart
In the IframeElement HTML <script>:
window.parent.postMessage(//data to send, "*");
and in Dart:
html.window.onMessage.listen((event) {
var data = event.data;
});
Again, I use JSON to communicate because I think it makes things easier. Use JSON.stringify() in JS and json.decode() in Dart.
Related
Good day all, I'm trying to use expo speech to read text inside my webview.
Since I can't use javascript speech synthesis inside the webview, it makes it a bit difficult to crack.
Following the example from the expo documentation for speech, I have the code below, i'm also injecting the speech into the html with this:
${speak(utterance)};
The error is can't find variable utterance, I know why the error, but I don't know how to pass the text to the function speak.
Please check my code below.
Thanks
import * as React from 'react';
import { View, StyleSheet, Button } from 'react-native';
import * as Speech from 'expo-speech';
import { WebView } from "react-native-webview";
export default function App() {
const speak = (thingToSay) => {
Speech.speak(thingToSay);
};
const html = `
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Epub Reader</title>
</head>
<body>
<div id="toolbar">
<button id=play></button>
<button id=pause></button>
<button id=stop></button>
</div>
<div id="area">
<p id="readme">
Content to be read
</p>
</div>
<script>
onload = function() {
var playEle = document.querySelector('#play');
var pauseEle = document.querySelector('#pause');
var stopEle = document.querySelector('#stop');
var flag = false;
playEle.addEventListener('click', onClickPlay);
pauseEle.addEventListener('click', onClickPause);
stopEle.addEventListener('click', onClickStop);
function onClickPlay() {
if (!flag) {
flag = true;
utterance = document.getElementsById('readme').innerText;
${speak(utterance)};
}
}
}
</script>
</body>
</html>`;
return (
<View style={{ flex: 1 }}>
<WebView source={{ html: html }} javaScriptEnabled={true} />
</View>
);
}
ok, so after a lot of research, I was able to come across webview's window.ReactNativeWebView.postMessage and the onMessage prop from this blog: https://blog.logrocket.com/the-complete-guide-to-react-native-webview/
So all i needed to do was send a message back on button click from my html and pass the message to onMessage, which then calls the expo speech to read the sent back text.
Please check the code below for reference:
<script>
var playEle = document.querySelector('#play');
var pauseEle = document.querySelector('#pause');
var stopEle = document.querySelector('#stop');
playEle.addEventListener('click', onClickPlay);
pauseEle.addEventListener('click', onClickPause);
stopEle.addEventListener('click', onClickStop);
function onClickPlay() {
window.ReactNativeWebView.postMessage(document.getElementsByTagName('iframe')[0].contentWindow.document.body.innerText);
}
function onClickPause() {
window.ReactNativeWebView.postMessage('pause');
}
function onClickStop() {
window.ReactNativeWebView.postMessage('stop');
}
and from the webview:
onMessage(m) {
if(m.nativeEvent.data === "pause"){
Speech.pause();
} else if(m.nativeEvent.data === "stop"){
Speech.stop();
}else{
Speech.speak(m.nativeEvent.data);
}
}
<WebView
source={{ html: html }}
javaScriptEnabled={true}
onMessage={m => this.onMessage(m)}
/>
I have multiple scripts in my HTML header. the two of concern are as follows:
1) JS script ('Infected Data') produces an object with data. The data is retrieved and computed from a google scripts file, so naturally it takes a bit.
2) A script which generates a map. The map is color coded depending on the values of the Infected Object Data.
The problem is the map loads before i can get the object, so it is not colored.
Map should look like this:
Map looks like this:
HTML Header:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>JQVMap - World Map</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<link href="../dist/jqvmap.css" media="screen" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript" src="../dist/jquery.vmap.js"></script>
<script type="text/javascript" src="../dist/maps/jquery.vmap.world.js" charset="utf-8"></script>
<script type="text/javascript" src="js/jquery.vmap.sampledata.deaths.js"></script>
<script type="text/javascript" src="js/jquery.vmap.sampledata.infected.js"></script>
<script>
jQuery(document).ready(function () {
jQuery('#vmap').vectorMap({
map: 'world_en',
backgroundColor: '#333333',
color: '#ffffff',
hoverOpacity: 0.8,
selectedColor: '#3498DB',
enableZoom: true,
showTooltip: true,
scaleColors: ['#F3A291', '#FF4F3B'],
values: infected_data,
normalizeFunction: 'polynomial',
onLabelShow: function(event, label, code)
{
// Remove for Russian Joke
/*if (code == 'ru')
{
// Plain TEXT labels
label.text('Bears, vodka, balalaika');
}
else*/
label.html('<div class="map-tooltip"><h1 class="header">'+label.html()+'</h1><p class="description">Infected: '+infected_data[code]+'</p><p class="description">Deaths: '+death_data[code]+'</p></div>');
/*else if (code == 'us')
{
label.html(label.html()+' (GDP - '+sample_data[code]+')');
}*/
},
/*onRegionOver: function(event, code)
{
if (code == 'ca')
{
event.preventDefault();
}
}, */
});
});
</script>
</head>
Infected Data JS FIle:
var infected_dataINT = {};
var infected_data = {};
const url = "https://script.google.com/macros/s/AKfycbzsyQNJwDvQc5SvNGEDZZOoNI3XxNar9PA9sRucZx7mgzfWpFQ/exec";
// Declare an async function
const getData = async () => {
// Use the await keyword to let JS know this variable has some latency so it should wait for it to be filled
// When the variable is fetched, use the .then() callback to carry on
const DataJSON = await fetch(url).then(response =>
response.json()
)
return await DataJSON
};
console.log(getData());
getData().then(result => {
console.log(result);
infected_dataINT = result;
console.log(infected_dataINT);
function toString(o) {
Object.keys(o).forEach(k => {
if (typeof o[k] === 'object') {
return toString(o[k]);
}
o[k] = '' + o[k];
});
return o;
}
console.log(toString(infected_dataINT));
infected_data = toString(infected_dataINT);
})
How can i slow down the jQuery(document).ready(function () {.... to run only after <script type="text/javascript" src="js/jquery.vmap.sampledata.infected.js"></script> has ran
You can dynamically append the script element to the document after the response has been recieved from the server like this:
let script = document.createElement('script');
script.src = 'myJqueryFile.js';
document.head.appendChild(script);
You just have to put those jquery codes inside a .js file.
Sounds like an asynch problem...
Where do you close the header?
</head>
And where is your onload event to synchrinise things?
<body onload="Function_That_KickStarts_Everything();">
Please use the correct document structure and ensure everything begins with the ONLOAD event so that 3rd party libraries may all load and synchronize... follow this please:
<html>
<head>
<style type="text/css">
</style>
</head>
<body onload="Function_That_KickStarts_Everything();">
<script src="Third_Party_Library_1.js"></script>
<script src="Third_Party_Library_2.js"></script>
<script type="text/javascript">
</script>
</body>
</html>
I have a flash swf of 1.2mb.
am embeding it with swfobject using dynamic embeding .
<script type="text/javascript">
var flashvars = {};
flashvars.campaignid = "12345678890";
var params = {};
params.allowscriptaccess = "always";
var attributes = {};
swfobject.embedSWF("soccer.swf", "myAlternativeContent", "550", "400", "10.0.0", false, flashvars, params, attributes);
</script>
am tring to read campaignid inside my document class ...
the code is like
public function Main()
{
loaderInfo.addEventListener(ProgressEvent.PROGRESS,update);
loaderInfo.addEventListener(Event.COMPLETE,onLoadedMovie);
}
private function update(e:ProgressEvent):void
{
}
private function onLoadedMovie(e:Event)
{
campId=this.root.loaderInfo.parameters["campaignid"];
}
when i alert the value i got null
when i use the same method in a small file it works..
can anyone help me?
regards
I got the answer by adding variable in the embed code. like this
swfobject.embedSWF("soccer.swf?campaignid=1234556"", "myAlternativeContent", "550", "400", "10.0.0", false, flashvars, params, attributes);
Thanks for the help :)
Adam Harte's answer is correct, I think the problem lies somewhere within your AS3 code, the following especially confused me:
public function Main()
{
loaderInfo.addEventListener(ProgressEvent.PROGRESS,update);
loaderInfo.addEventListener(Event.COMPLETE,onLoadedMovie);
}
private function update(e:ProgressEvent):void { }
private function onLoadedMovie(e:Event)
{
campId=this.root.loaderInfo.parameters["campaignid"];
}
I've created a simple(and working) example of how your code should look:
index.html:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>FlashVars</title>
<meta name="language" content="en" />
<meta name="description" content="" />
<meta name="keywords" content="" />
<script src="js/swfobject.js" type="text/javascript"></script>
<script type="text/javascript">
var flashvars = { campaignid: "12345678890" };
var params = { menu: "false", scale: "noScale", allowFullscreen: "true", allowScriptAccess: "always", bgcolor: "", wmode: "direct" };
var attributes = { id:"FlashVars" };
swfobject.embedSWF("FlashVars.swf", "altContent", "100%", "100%", "10.0.0", "expressInstall.swf", flashvars, params, attributes);
</script>
<style type="text/css">
html, body { height:100%; overflow:hidden; }
body { margin:0; }
</style>
</head>
<body>
<div id="altContent">
<h1>FlashVars</h1>
<p>Alternative content</p>
<p>
<a href="http://www.adobe.com/go/getflashplayer">
<img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" />
</a>
</p>
</div>
</body>
</html>
Main.as(document class):
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
public class Main extends Sprite
{
public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}// end function
private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
if (loaderInfo.parameters.campaignid)
{
var textField:TextField = new TextField();
textField.autoSize = TextFieldAutoSize.LEFT;
textField.text = loaderInfo.parameters.campaignid;
addChild(textField);
}// end if
}// end function
}// end class
}// end package
The following is an image of the example beening run in a browser:
Maybe try getting the var after your Main class has been added to the stage. This will make sure everything is loaded and ready. Try something like this:
public function Main()
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
var campId:String = this.root.loaderInfo.parameters["campaignid"];
trace('campId', campId);
}
I'm using JavaScript interface for checking if Google's StreetView is available. My problem is that from android 3.0 code stopped working, and I am unnable to find why. Problem is that methods from "JavascriptCheck" interface are never called and Logcat doesn't show any errors.
Java code:
public void showStreetView(GeoPoint geoPoint) {
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JavascriptCheck(), "Android");
lat = geoPoint.getLatitudeE6()/1E6;
lon = geoPoint.getLongitudeE6()/1E6;
webView.loadDataWithBaseURL("", context.getString(R.string.html_streetview, lat, lon), "text/html", "UTF-8", "");
}
public class JavascriptCheck {
public void hasStreetview(boolean hasStreetview) {
if (hasStreetview) {
openStreetView();
} else {
Toast.makeText(context, context.getString(R.string.loc_no_street_view), Toast.LENGTH_SHORT).show();
}
}
}
WebView in layout file:
<WebView android:id="#+id/webView"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone">
</WebView>
JavaScript string:
<string name="html_streetview">
<html>
<head>
<script src=\"http://maps.google.com/maps?file=api&v=2& sensor=false\" type=\"text/javascript\"/>
</head>
<body>
<script type=\"text/javascript\">
var testPoint = new GLatLng(%1$s, %2$s);
var svClient = new GStreetviewClient();
svClient.getNearestPanoramaLatLng(testPoint, function (nearest) {
if ((nearest !== null) && (testPoint.distanceFrom(nearest) <= 100)) {
Android.hasStreetview(true);
} else {
Android.hasStreetview(false);
}
});
</script>
</body>
</html>
</string>
Solved my problem long ago, just wanted to share with others. Honeycomb and later Android versions require that you use full html <script> tags. Also it is better to keep script string in assets folder. My assets/index.html looks like this now:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<script src="http://maps.googleapis.com/maps/api/js?sensor=false" type="text/javascript"></script>
<script type="text/javascript">
var sv = new google.maps.StreetViewService();
function hasStreet(lat, lon) {
var point = new google.maps.LatLng(lat, lon);
sv.getPanoramaByLocation(point, 50, isSVAvailable);
}
function isSVAvailable(data, status) {
if (status == google.maps.StreetViewStatus.OK) {
Android.hasStreetview(true);
} else {
Android.hasStreetview(false);
}
}
</script>
</head>
<body></body>
</html>
I too was using this function and have seen it broken since I tried upgrading my app for ICS. It seems that the Javascript won't execute if you have an external src link. If you take out the javascript src link and add some logging you'll see that the script will run (and obviously return false all the time).
I know in the docs they recommend not using javascript that calls into your native code unless you control all elements in the javascript but perhaps now they explicitly stop code from running that references an external resource?
If i have such javascript in my razor veiw:
#{
Grid grid = #Model.GetGridFromModel();
Bool isSomething = #Model.GetSomething();
Bool isSomethingMore = #Model.GetSomehtingMore();
Bool isSomethingElse = #Model.GetSomethingElse()
int caseCount = 0;
}
$(document).ready(function () {
$("#tabs").tabs({
show: function (event, ui) {
switch (ui.index) {
#if (isSomething){
<text>
case #caseCount:
change('#grid.Avalue');
break;
</text>
caseCount++;
}
#if(isSomethingElse){
<text>
case #caseCount:
change('#grid.Bvalue');
break;
</text>
caseCount++;
}
#if (isSomethingElseMore){
<text>
case #caseCount:
change('#grid.Cvalue');
break;
</text>
}
}
}
});
funciton change(id)
{
//doing somehting;
}
So i want to put that javascript in separate file and reference that file to my view, and the problem is how may i pass values from razor to javascript when javascript in separate file?
Javascript are files without being parsed by the compiler, so, you have no chance...
What you can do however is to use a dynamic javascript, for example:
<script src="/CustomScripts/scripts.js"><script>
Have a route that says:
routes.MapRoute(
"CustomScripts", "CustomScripts/{id}",
new { controller = "Scripts", action = "GetFile" }
);
Create your controler and use a simple return View(); like
public ActionResult GetFile(string id)
{
// use id as you please
// pass any Model you want
return View();
}
In that view, just put your javascript with Razor syntax.
Or you can use variables and load them up make the use of RenderSection()
in your _Layout.cshtml file add a section in your <head> before any other javascript
<head>
<meta charset="utf-8" />
<title>#ViewBag.Title</title>
<link href="#Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
#RenderSection("script_variables", false)
<script src="#Url.Content("~/Scripts/jquery-1.6.2.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/modernizr-2.0.6-development-only.js")" type="text/javascript"></script>
</head>
and in any View that you want to add such variables, just do:
#Section script_variables {
<script type="text/javascript">
var variableA = '#MyVarA',
variableB = '#MyVarB',
variableC = '#MyVarC';
</script>
}
And all other files that you load the script will have such code...
You might declare some js-variables on your page and then use those variables from the .js-file
You might call a function inside your .js-file and send your data as arguments
(Please consider creating one object and fill with your data instead of creating multiple variables.)