The embedded WebView browser I am using needs special handling for particular URLs, to open them in the native default browser instead of WebView. The actual browsing part w
There is another method for handling this.
You can add an event listener to the DOM elements and intercept it that way.
Example:
NodeList nodeList = document.getElementsByTagName("a");
for (int i = 0; i < nodeList.getLength(); i++)
{
Node node= nodeList.item(i);
EventTarget eventTarget = (EventTarget) node;
eventTarget.addEventListener("click", new EventListener()
{
@Override
public void handleEvent(Event evt)
{
EventTarget target = evt.getCurrentTarget();
HTMLAnchorElement anchorElement = (HTMLAnchorElement) target;
String href = anchorElement.getHref();
//handle opening URL outside JavaFX WebView
System.out.println(href);
evt.preventDefault();
}
}, false);
}
Where document is the DOM document object. Make sure this is done after the document has finished loading.
I found another solution:
I implemented it so the secondary WebEngine is initialized lazy. It may also be initialized in the constructor. Both has pros and contras.
Note: This only triggers for Links which open as a popup. This usually is the case when an a-element has a target-attribute that is not "_self" or with JS: window.open(...)
.
Here is the Magic ...
Register it like this:
engine.setCreatePopupHandler(new BrowserPopupHandler());
The core class:
public static class BrowserPopupHandler implements Callback<PopupFeatures, WebEngine>
{
private WebEngine popupHandlerEngine;
public WebEngine call(PopupFeatures popupFeatures)
{
// by returning null here the action would be canceled
// by returning a different WebEngine (than the main one where we register our listener) the load-call will go to that one
// we return a different WebEngine here and register a location change listener on it (see blow)
return getPopupHandler();
}
private WebEngine getPopupHandler()
{
if (popupHandlerEngine == null) // lazy init - so we only initialize it when needed ...
{
synchronized (this) // double checked synchronization
{
if (popupHandlerEngine == null)
{
popupHandlerEngine = initEngine();
}
}
}
return popupHandlerEngine;
}
private WebEngine initEngine()
{
final WebEngine popupHandlerEngine = new WebEngine();
// this change listener will trigger when our secondary popupHandlerEngine starts to load the url ...
popupHandlerEngine.locationProperty().addListener(new ChangeListener<String>()
{
public void changed(ObservableValue<? extends String> observable, String oldValue, String location)
{
if (!location.isEmpty())
{
Platform.runLater(new Runnable()
{
public void run()
{
popupHandlerEngine.loadContent(""); // stop loading and unload the url
// -> does this internally: popupHandlerEngine.getLoadWorker().cancelAndReset();
}
});
try
{
// Open URL in Browser:
Desktop desktop = Desktop.getDesktop();
if (desktop.isSupported(Desktop.Action.BROWSE))
{
URI uri = new URI(location);
desktop.browse(uri);
}
else
{
System.out.println("Could not load URL: " + location);
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
});
return popupHandlerEngine;
}
}
2017 version - still very hacky but much more concise:
class AboutDialog extends Dialog {
private final Controller controller;
private final String url;
AboutDialog() {
super();
this.controller = Controller.getInstance();
this.setTitle(controller.getProperty("about_title"));
this.setHeaderText(null);
this.url = getClass().getResource("/about_dialog.html").toExternalForm();
this.setWebView();
this.getDialogPane().getButtonTypes().add(new ButtonType(controller.getProperty("close"), ButtonBar.ButtonData.CANCEL_CLOSE));
this.getDialogPane().setPrefWidth(600);
}
private void setWebView() {
final WebView webView = new WebView();
webView.getEngine().load(url);
webView.getEngine().locationProperty().addListener((observable, oldValue, newValue) -> {
controller.getMainFxApp().getHostServices().showDocument(newValue);
Platform.runLater(this::setWebView);
});
this.getDialogPane().setContent(webView);
}
}
Sorry for digging out this old thread but I found another solution that I wanted to share with others who struggle with the same problem. I found a library that has a nice wrapper around the entire issue, see its docs at github.
Edit:
Oh, sry for not telling what the project does:
The linked library contains a class that actually implemented all of the code discussed in this thread. The user can simply create a new instance of the WebViewHyperlinkListener
-interface that gets automatically called when something (mouse enter, mouse quit, mouse click) happens with the link. Once the handler terminates, it returns a boolean: If the handler returns true
, the WebView will not navigate to the linked web page. If the handler returns false
, it will.
I finally found a working solution that worked for me:
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.web.WebView;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLAnchorElement;
import java.awt.*;
import java.net.URI;
public class HyperLinkRedirectListener implements ChangeListener<Worker.State>, EventListener
{
private static final String CLICK_EVENT = "click";
private static final String ANCHOR_TAG = "a";
private final WebView webView;
public HyperLinkRedirectListener(WebView webView)
{
this.webView = webView;
}
@Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue)
{
if (Worker.State.SUCCEEDED.equals(newValue))
{
Document document = webView.getEngine().getDocument();
NodeList anchors = document.getElementsByTagName(ANCHOR_TAG);
for (int i = 0; i < anchors.getLength(); i++)
{
Node node = anchors.item(i);
EventTarget eventTarget = (EventTarget) node;
eventTarget.addEventListener(CLICK_EVENT, this, false);
}
}
}
@Override
public void handleEvent(Event event)
{
HTMLAnchorElement anchorElement = (HTMLAnchorElement) event.getCurrentTarget();
String href = anchorElement.getHref();
if (Desktop.isDesktopSupported())
{
openLinkInSystemBrowser(href);
} else
{
// LOGGER.warn("OS does not support desktop operations like browsing. Cannot open link '{}'.", href);
}
event.preventDefault();
}
private void openLinkInSystemBrowser(String url)
{
// LOGGER.debug("Opening link '{}' in default system browser.", url);
try
{
URI uri = new URI(url);
Desktop.getDesktop().browse(uri);
} catch (Throwable e)
{
// LOGGER.error("Error on opening link '{}' in system browser.", url);
}
}
}
Usage:
webView.getEngine().getLoadWorker().stateProperty().addListener(new HyperLinkRedirectListener(webView));
When wrapping the d.browse call into a Runnable Object the runtime error never occured again. The strange thing was without that wrapping the ChangeListener was called a second time after some seconds with the same new location and this second call crashed the JVM.