I've setup a Javascript-Java bridge for WebView:
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("JavaBridge", bridge);
});
The bridge works fine on the first content load.
On subsequent content loads, the bridge is delayed, and is not recognized until after the rest of the Javascript is loaded.
For example, if I set up a bridge "JavaBridge" and give it a method "log()", the following HTML will work fine on the first content load, but will fail on the second:
<script>JavaBridge.log('On content load')</script>
The bridge is still loaded eventually, just too late for my purpose.
Is there a way to modify my bridge so it is always setup before any Javascript code is ran?
There is also a chance my issue is related to these bug reports.
Full example:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebEvent;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;
public class Main extends Application {
private Bridge bridge = new Bridge();
private static final String BRIDGE_NAME = "JavaBridge";
private static final String HTML =
" <script>try{"
+ BRIDGE_NAME + ".log('On content load');" // try to log a message on content load
+ "} catch(err) {alert(err.message);}" // alert message if bridge is not yet defined
+ "</script>"
+ "<button onclick=\"" + BRIDGE_NAME + ".log('Button clicked')\">Log Click</button>";
@Override
public void start(Stage stage) {
// Setup the Webview
WebView webView = new WebView();
webView.setPrefSize(100,100);
WebEngine webEngine = webView.getEngine();
webEngine.setOnAlert(Main::showAlert);
// Setup the bridge
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember(BRIDGE_NAME, bridge);
});
// Load button, press twice or more to see the bug
Button button = new Button("Load content");
button.setOnAction(actionEvent -> {
webEngine.loadContent(HTML);
});
// Setup stage and scene
HBox hbox = new HBox(button, webView);
Scene scene = new Scene(hbox);
stage.setScene(scene);
stage.show();
}
// The Java bridge. Has a "log" method.
public static class Bridge {
public void log(String msg) {
System.out.println("Invoked from JavaScript: " + msg);
}
}
// Shows the alert, used in JS catch statement
private static void showAlert(WebEvent<String> event) {
javafx.scene.control.Dialog<ButtonType> alert = new javafx.scene.control.Dialog<>();
alert.getDialogPane().setContentText(event.getData());
alert.getDialogPane().getButtonTypes().add(ButtonType.OK);
alert.showAndWait();
}
public static void main(String[] args) {
launch(args);
}
}
If you click the "Load content" button, it will print the expected message in the console:
Invoked from JavaScript: On content load
If however you click on the "Load content" button a second time, it doesn't print the message to the console, instead giving the alert message:
Can't find variable: JavaBridge
Strangely, the bridge appears to be eventually loaded, as clicking on the "Log click" button gives the correct printed message through the bridge:
Invoked from JavaScript: Button clicked
EDIT: Further tests showed that on the first content load, window.setMember()
may be set at anytime before newState
reaches State.SUCCEEDED
.
On subsequent content loads, window.setMember()
is ignored/overridden once State.SUCCEEDED
is reached. A workaround is to create a listener so that window.setMember()
is set after State.SUCCEEDED
is reached; however, because the bridge is set so late, all the other Javascript stuff will have already ran.