X

Porting from the Browser to Nashorn/JavaFX (Part I)

By: Jim Laskey | Senior Development Manager

During the JavaOne Nashorn: JavaScript for the JVM session, I showed a couple examples of converting JavaScript browser examples to use Nashorn and JavaFX. The first example was using CKEditor, a rich text/HTML editor. This editor is written entirely in JavaScript using the browser DOM interface. In this case, it did not really make sense to convert all the DOM code over to FX. Instead, I used the JavaFX WebView which supports all the DOM needed.

So let's start with a simple example to bring up a WebView (run with jjs -fx -scripting example1.js);
// example1.js
var Scene = Java.type("javafx.scene.Scene");
var WebView = Java.type("javafx.scene.web.WebView");
var site = "http://www.oracle.com";
var webView = new WebView();
webView.engine.load(site);
$STAGE.title = site;
$STAGE.scene = new Scene(webView, 800, 600);
$STAGE.show();
The next example brings up a WebView with a CKEditor contained within. You can download the CKEditor toolkit from http://ckeditor.com/download. Place the ckeditor folder in your working directory.
<!-- example2.html -->
<!DOCTYPE html>
<html>
<head>
<script src="./ckeditor/ckeditor.js"></script>
</head>
<body>
<form id="MainForm" method="post">
<textarea id="MainEditor">
&lt;h1&gt;Example&lt;/h1&gt;
&lt;p&gt;Some content for the Demo&lt;/p&gt;
</textarea>
<script>
CKEDITOR.replace("MainEditor");
</script>
</form>
</body>
</html>
// example2.js
var Scene = Java.type("javafx.scene.Scene");
var WebView = Java.type("javafx.scene.web.WebView");
var link = "file:${$ENV.PWD}/example2.html";
var webView = new WebView();
webView.engine.load(link);
$STAGE.title = "CKEditor";
$STAGE.scene = new Scene(webView, 1024, 360);
$STAGE.show();

  

For the next part, it gets a little confusing, but you should be able to catch on to the weirdness (after all, you're a JavaScript developer.) First, you have to be aware of the fact that there is a separate JavaScriptCore engine inside the WebView. The example shows interaction with this engine by asking it to load an URL.

Second, we want to be able to have JSObject objects passed back from the WebView engine and have them behave like Nashorn objects. To accomplish this we need to "wrap" the JSObject objects in Nashorn JSAdaptor objects.
function wrap(jsobject) {
return new JSAdapter({
__get__ : function (key) {
return jsobject.getMember(key);
},
__has__ : function (key) {
return jsobject.call("hasOwnProperty", key);
},
__put__ : function (key, value) {
return jsobject.setMember(key, value);
},
__call__ : function (name) {
var args = Array.prototype.slice.call(arguments);
args.shift();
args = Java.to(args, ObjectArray);
return jsobject.call(name, args);
},
__new__ : function () {
return wrap(jsobject.eval("new this()"));
},
__delete__ : function (key) {
jsobject.removeMember(key);
return true;
},
__getIds__ : function () {
return jsobject.eval("Object.keys(this)");
}
});
}
The wrap function creates a closure containing "jsobject" and the adaptor functions specified. This will allow you to do things like;
var window = wrap(webview.engine.executeScript("window"));
var document = wrap(window.document);
The adaptor above is very basic. The complete code might look more like;
var ObjectArray = Java.type("java.lang.Object[]");
var NetscapeJSObject = Java.type("netscape.javascript.JSObject");
var EventHandler = Java.type("javafx.event.EventHandler");
// Lambda functions for 0, 1, or 2 arguments (no need for external java code.)
var Supplier = Java.type("java.util.function.Supplier");
var UniFunction = Java.type("java.util.function.Function");
var BiFunction = Java.type("java.util.function.BiFunction");
function wrap(jsobject) {
// If not a JSObject type (primitives) then don't wrap.
if (!(jsobject instanceof NetscapeJSObject)) {
return jsobject;
}
// Construct a SAM to call back into Nashorn (event handling)
function callback(func) {
if (typeof func == 'function') {
switch (func.length) {
case 0:
return new (Java.extend(Supplier, { get: function() { return func(); } }))();
case 1:
return new (Java.extend(UniFunction, { apply: function(arg) { return func(wrap(arg)); } }))();
case 2:
return new (Java.extend(BiFunction, { apply: function(arg1, arg2) { return func(wrap(arg1), wrap(arg2)); }}))();
}
}
throw "Callbacks can only have zero, one or two arguments";
}
// Potentially wrap arguments for a call.
function wrapArgs(args) {
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (arg) {
if (arg.unwrap) {
args[i] = arg.unwrap();
} else if (typeof arg == "function") {
args[i] = callback(arg);
}
}
}
}
return new JSAdapter({
__get__ : function (key) {
// special case unwrap to return the jsobject
if (key == "unwrap") {
return function() { return jsobject };
}
// Handle number indexing
var value = typeof key == "number" ? jsobject.getSlot(key) : jsobject.getMember(key);
// Wrap the result
return wrap(value);
},
__has__ : function (key) {
return jsobject.call("hasOwnProperty", key);
},
__put__ : function (key, value) {
// Wrap functions as callback SAMs
if (typeof value == "function") {
value = callback(value);
}
// Handle number indexing
return typeof key == "number" ? jsobject.setSlot(key, value) : jsobject.setMember(key, value);
},
__call__ : function (name) {
// Special case unwrap to return the jsobject
if (name == "unwrap") {
return jsobject;
}
var args = Array.prototype.slice.call(arguments);
args.shift();
args = Java.to(args, ObjectArray);
wrapArgs(args);
return jsobject.call(name, args);
},
__new__ : function () {
return wrap(jsobject.eval("new this()"));
},
__delete__ : function (key) {
jsobject.removeMember(key);
return true;
},
__getIds__ : function () {
// Convert the Node collection to a JS array
var keys = jsobject.eval("Object.keys(this)");
var length = keys.getMember("length");
var ids = [];
for (var i = 0; i < length; i++) {
ids.push(keys.getSlot(i));
}
return ids;
}
});
}
I know this code looks complex, but you only have to write once and include (load) in WebView related projects. Note that the code snippet above then becomes;
var window = wrap(webview.engine.executeScript("window"));
var document = window.document;
In the example I also created a special helper wrapper for the WebView to simplify accessing the DOM.
function WebViewWrapper(onload) {
var This = this;
var WebView = Java.type("javafx.scene.web.WebView");
var webview = new WebView();
This.webview = webview;
This.engine = webview.engine;
This.window = undefined;
This.document = undefined;
// Make sure the JavaScript is enabled.
This.engine.javaScriptEnabled = true;
// Complete initialization when page is loaded.
This.engine.loadWorker.stateProperty().addListener(new ChangeListener() {
changed: function(value, oldState, newState) {
if (newState == Worker.State.SUCCEEDED) {
This.document = wrap(This.engine.executeScript("document"));
This.window = wrap(This.engine.executeScript("window"));
// Call users onload function.
if (onload) {
onload(This);
}
}
}
});
// Divert alert message to print.
This.engine.onAlert = new EventHandler() {
handle: function(evt) {
print(evt.data);
}
};
// Load page from URL.
This.load = function(url) {
This.engine.load(url);
}
// Load page from text.
This.loadContent = function(text) {
This.engine.loadContent(text);
}
}
So now the main script looks as follows;
var Scene = Java.type("javafx.scene.Scene");
var link = "file:${$ENV.PWD}/example2.html";
var wvw = WebViewWrapper();
wvw.load(link);
$STAGE.title = "CKEditor";
$STAGE.scene = new Scene(wvw.webview, 1024, 360);
$STAGE.show();
Getting the content from the CKEditor;
var window = wvw.window;
var CKEDITOR = window.CKEDITOR;
var mainEditor = CKEDITOR.instances.MainEditor;
var text = mainEditor.getData();
Setting the content in the CKEditor;
var window = wvw.window;
var CKEDITOR = window.CKEDITOR;
var mainEditor = CKEDITOR.instances.MainEditor;
mainEditor.setData(text);

Join the discussion

Comments ( 5 )
  • guest Tuesday, December 16, 2014

    Running the last version of the code I get a

    ReferenceError: "ChangeListener" is not defined

    What is missing?


  • jlaskey Tuesday, December 16, 2014

    It's declared in web.js. In this example I used;

    load("fx:base.js");

    load("fx:controls.js");

    load("fx:graphics.js");

    load("fx:web.js");


  • guest Tuesday, April 28, 2015

    I´m trying to run your code, but directly trhough Java and not jjs. I got example1 and example2 to work fine, but I don´t know how to create a WebViewWrapper in Java. How do I load the adapter.js file in java and call it?

    Thanks,

    Rodrigo


  • jlaskey Tuesday, April 28, 2015

    I think I need more explanation of what you are trying to do.


  • Rodrigo Wednesday, April 29, 2015

    I´m now stuck on the same ChangeListener error as above. Where do I add the load methods? I tried to add the load methods in the adapter.js file, but it doesn´t work. It, of course, it doesn´t work if I try to add it on the java file. In java should it be an import <something>?


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha