javascriptjavajavafxjavafx-webenginejavafx-webview

JavaFx WebView element selection. (JavaScript)


I have a WebView element, which render a html code. I want to create getSelectedIndices and setSelectedIndices method. Getter works, Setter throws IndexSizeException.

How can I set the selection area of WebView?

package hu.krisz;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class WebViewExample extends Application {
    private String content = "<h1>Title</h1> <h2>SubTitle</h2> <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</p>";

    @Override
    public void start(Stage primaryStage) {
        WebView webView = new WebView();
        webView.getEngine().loadContent(content);

        // Create button.
        Button button = new Button("Select");

        // Button EventHandler.
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                // Work correctly.
                int[] indeices = getSelectedIndices(webView);
                System.out.println(indeices[0] + ", " + indeices[1]);

                // netscape.javascript.JSException: IndexSizeError: The index is not in the allowed range.
                // indeices[0] = 1;
                // indeices[1] = 4;
                setSelectedIndices(webView, indeices);
            }
        });
        // Create VBox and add the button.
        VBox vbox = new VBox(button);

        // Create BorderPane and add VBox.
        BorderPane root = new BorderPane();
        root.setTop(vbox);
        root.setCenter(webView);

        // Create Scene
        Scene scene = new Scene(root, 800, 600);

        // Setting Stage
        primaryStage.setTitle("WebView Példa");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    public int[] getSelectedIndices(WebView webView) {
        String script = "var selection = window.getSelection();" +
                "if (selection.rangeCount > 0) {" +
                "    var range = selection.getRangeAt(0);" +
                "    var preSelectionRange = range.cloneRange();" +
                "    preSelectionRange.selectNodeContents(document.body);" +
                "    preSelectionRange.setEnd(range.startContainer, range.startOffset);" +
                "    var start = preSelectionRange.toString().length;" +
                "    var end = start + range.toString().length;" +
                "    start + ',' + end;" +
                "} else {" +
                "    '0,0';" +
                "}";

        String result = (String) webView.getEngine().executeScript(script);
        int[] indices = new int[2];
        if (result != null && !result.isEmpty()) {
            String[] splitResult = result.split(",");
            indices[0] = Integer.parseInt(splitResult[0]);
            indices[1] = Integer.parseInt(splitResult[1]);
        } else {
            indices[0] = 0;
            indices[1] = 0;
        }
        return indices;
    }

    public void setSelectedIndices(WebView webView, int[] indices) {
        String script = "function selectText(start, end) {" +
                "    var range = document.createRange();" +
                "    var selection = window.getSelection();" +
                "    range.setStart(document.body.firstChild, start);" +
                "    range.setEnd(document.body.firstChild, end);" +
                "    selection.removeAllRanges();" +
                "    selection.addRange(range);" +
                "}" +
                "selectText(" + indices[0] + ", " + indices[1] + ");";

        webView.getEngine().executeScript(script);
        System.out.println("SELECTED FINSIHED.");
    }
}

I tried to changed the Js script.

String script = "function selectText(start, end) {" +
    "    var node = document.body;" +
    "    var range = document.createRange();" +
    "    var selection = window.getSelection();" +
    "    range.setStart(node, start);" +
    "    range.setEnd(node, end);" +
    "    selection.removeAllRanges();" +
    "    selection.addRange(range);" +
    "}" +
    "selectText(" + indices[0] + ", " + indices[1] + ");";

This way, the method doesn't always throw errors, but either doesn't select anything or doesn't select the specified range


Solution

  • Check out this component I wrote. It can highlight text in a webview with a searchbar and remove the highlightings again. You can alter it to your needs.

    https://github.com/davidweber411/WedasoftFxCustomNodes

    package com.wedasoft.wedasoftFxCustomNodes.htmlViewer;
    
    import javafx.geometry.Insets;
    import javafx.scene.control.Button;
    import javafx.scene.control.TextField;
    import javafx.scene.input.KeyCode;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.web.WebEngine;
    import javafx.scene.web.WebView;
    import lombok.Getter;
    
    import java.net.URL;
    
    @Getter
    public class HtmlViewer extends BorderPane {
    
        private final WebView webView;
        private final TextField searchTextField;
        private final JavascriptBridge javascriptBridge;
        private final Button searchButton;
        private final Button selectPreviousSearchResultButton;
        private final Button selectNextSearchResultButton;
        private final Button resetButton;
    
        private int selectedSearchResultIndex;
    
        public HtmlViewer(URL url) {
            webView = new WebView();
            webView.setMaxHeight(Integer.MAX_VALUE);
            webView.setMaxWidth(Integer.MAX_VALUE);
            webView.getEngine().setJavaScriptEnabled(true);
            setCenter(webView);
    
            searchTextField = new TextField();
            searchTextField.setPromptText("Search...");
            searchTextField.setMinWidth(0);
            searchTextField.setMinHeight(0);
    
            javascriptBridge = new JavascriptBridge(webView);
    
            searchButton = new Button("Search");
            searchButton.setMinWidth(0);
            searchButton.setMinHeight(0);
            searchButton.setOnAction(e -> onSearchButtonClick());
    
            selectPreviousSearchResultButton = new Button("/\\");
            selectPreviousSearchResultButton.setOnAction(e -> onSelectPreviousSearchResultButtonClick());
    
            selectNextSearchResultButton = new Button("\\/");
            selectNextSearchResultButton.setOnAction(e -> onSelectNextSearchResultButtonClick());
    
            resetButton = new Button("Reset");
            resetButton.setMinWidth(0);
            resetButton.setMinHeight(0);
            resetButton.setOnAction(e -> onResetButtonClick());
    
            HBox searchBar = new HBox(
                    searchTextField,
                    searchButton,
                    selectPreviousSearchResultButton,
                    selectNextSearchResultButton,
                    resetButton);
            HBox.setHgrow(searchTextField, Priority.ALWAYS);
            searchBar.setMinHeight(0);
            searchBar.setMaxWidth(Double.MAX_VALUE);
            searchBar.setSpacing(5);
            searchBar.setPadding(new Insets(5, 5, 5, 5));
            setTop(searchBar);
    
            loadHtmlByUrl(url);
    
            setOnKeyReleased(e -> {
                if (e.getCode() == KeyCode.ENTER) onSearchButtonClick();
                if (e.getCode() == KeyCode.F && e.isControlDown()) onSearchShortcutClick();
            });
        }
    
        private void onSelectNextSearchResultButtonClick() {
            selectedSearchResultIndex = javascriptBridge.selectNextSearchResultAndReturnNewIndex(selectedSearchResultIndex);
        }
    
        private void onSelectPreviousSearchResultButtonClick() {
            selectedSearchResultIndex = javascriptBridge.selectPreviousSearchResultAndReturnNewIndex(selectedSearchResultIndex);
        }
    
    
        public void loadHtmlByUrl(URL url) {
            webView.getEngine().load(url.toExternalForm());
            selectedSearchResultIndex = -1;
        }
    
        private void onSearchShortcutClick() {
            webView.requestFocus();
            searchTextField.requestFocus();
        }
    
        private void onSearchButtonClick() {
            javascriptBridge.removeHighlightings();
            if (searchTextField.getText().trim().isBlank()) {
                return;
            }
            javascriptBridge.highlightText(searchTextField.getText());
            selectedSearchResultIndex = -1;
            onSelectNextSearchResultButtonClick();
        }
    
        private void onResetButtonClick() {
            javascriptBridge.removeHighlightings();
            selectedSearchResultIndex = -1;
            searchTextField.setText("");
        }
    
        @SuppressWarnings("unused")
        private String getCurrentHtmlOfWebViewDocument() {
            return javascriptBridge.getDomAsHtmlString();
        }
    
        private static class JavascriptBridge {
            private final WebEngine webEngine;
            private final String nodeContainingHighlightings;
            private final boolean caseInSensitive;
            private final String cssClassUsedForHighlighting;
    
            public JavascriptBridge(final WebView webView) {
                this.webEngine = webView.getEngine();
                this.nodeContainingHighlightings = "document.body";
                this.caseInSensitive = true;
                this.cssClassUsedForHighlighting = "wedasoft-highlighted";
            }
    
            void highlightText(String textToHighlight) {
                final String javascript = String.format("""
                                 /**
                                 * @param {string} elem Element to search for keywords in
                                 * @param {string[]} keywords Keywords to highlight
                                 * @param {boolean} caseInSensitive Differenciate between capital and lowercase letters
                                 * @param {string} cssClass Class to apply to the highlighted keyword
                                 */
                                function highlightInsideElement(elem, keywords, caseInSensitive = true, cssClass) {
                                    const flags = caseInSensitive ? 'gi' : 'g';
                                    // Sort longer matches first to avoid
                                    // highlighting keywords within keywords.
                                    keywords.sort((a, b) => b.length - a.length);
                                    Array.from(elem.childNodes).forEach(child => {
                                        const keywordRegex = RegExp(keywords.join('|'), flags);
                                        if (child.nodeType !== 3) { // not a text node
                                            highlightInsideElement(child, keywords, caseInSensitive, cssClass);
                                        } else if (keywordRegex.test(child.textContent)) {
                                            const frag = document.createDocumentFragment();
                                            let lastIdx = 0;
                                            child.textContent.replace(keywordRegex, (match, idx) => {
                                                const part = document.createTextNode(child.textContent.slice(lastIdx, idx));
                                                const highlighted = document.createElement('span');
                                                highlighted.textContent = match;
                                                highlighted.classList.add(cssClass);
                                                highlighted.style.background='yellow';
                                                frag.appendChild(part);
                                                frag.appendChild(highlighted);
                                                lastIdx = idx + match.length;
                                            });
                                            const end = document.createTextNode(child.textContent.slice(lastIdx));
                                            frag.appendChild(end);
                                            child.parentNode.replaceChild(frag, child);
                                        }
                                    });
                                }
                                                       
                                highlightInsideElement(%s, ['%s'], %s, '%s');""",
                        nodeContainingHighlightings,
                        textToHighlight,
                        caseInSensitive,
                        cssClassUsedForHighlighting);
    
                webEngine.executeScript(javascript);
            }
    
            void removeHighlightings() {
                final String javascript = String.format("""
                                /**
                                 * @param {string} nodeToRemoveHighlightingsIn Node to remove highlights from.
                                 * @param {string} cssClass Class used for highlighting.
                                 */
                                function removeHighlightings(nodeToRemoveHighlightingsIn, cssClass) {
                                  Array.from(nodeToRemoveHighlightingsIn.querySelectorAll(`.${cssClass}`)).forEach(span => {
                                    const parent = span.parentNode;
                                    parent.replaceChild(document.createTextNode(span.textContent), span);
                                    parent.normalize();
                                  });
                                }
                                                
                                removeHighlightings(%s, '%s');""",
                        nodeContainingHighlightings,
                        cssClassUsedForHighlighting);
    
                webEngine.executeScript(javascript);
            }
    
            Integer selectNextSearchResultAndReturnNewIndex(int selectedSearchResultIndex) {
                return selectSearchResultAndReturnNewIndex(true, selectedSearchResultIndex);
            }
    
            Integer selectPreviousSearchResultAndReturnNewIndex(int selectedSearchResultIndex) {
                return selectSearchResultAndReturnNewIndex(false, selectedSearchResultIndex);
            }
    
            Integer selectSearchResultAndReturnNewIndex(boolean scrollToNextResult, int selectedSearchResultIndex) {
                final String javascript = String.format("""
                                /**
                                 * @param {string} cssClass The css class used for highlighting and to scroll to.
                                 * @param selectedSearchResultIndex {number} The index of the current 'selected' search result.
                                 * @param scrollToNext {boolean} True for scrolling to the next search result, false for the previous one.
                                 * @returns {number} The new current index of the selected search result.
                                 */
                                function scrollToSearchResult(cssClass, selectedSearchResultIndex, scrollToNext = true) {
                                    function scrollToResultAndMark(allResults, newResultIndex) {
                                        for (let result of allResults) {
                                            result.style.background = 'yellow';
                                        }
                                        allResults[newResultIndex].scrollIntoView(true);
                                        allResults[newResultIndex].style.background = '#FF9632';
                                        return newResultIndex;
                                    }
    
                                    let allResults = document.querySelectorAll('.' + cssClass);
    
                                    if (allResults.length === 0) {
                                        return -1;
                                    }
                                    if (selectedSearchResultIndex === -1) {
                                        let firstIndex = 0;
                                        return scrollToResultAndMark(allResults, firstIndex);
                                    }
    
                                    if (scrollToNext) {
                                        if (selectedSearchResultIndex < allResults.length - 1) {
                                            let nextIndex = selectedSearchResultIndex + 1;
                                            return scrollToResultAndMark(allResults, nextIndex);
                                        } else {
                                            let firstIndex = 0;
                                            return scrollToResultAndMark(allResults, firstIndex);
                                        }
                                    } else {
                                        if (selectedSearchResultIndex > 0) {
                                            let previousIndex = selectedSearchResultIndex - 1;
                                            return scrollToResultAndMark(allResults, previousIndex);
                                        } else {
                                            let lastIndex = allResults.length - 1;
                                            return scrollToResultAndMark(allResults, lastIndex);
                                        }
                                    }
                                }
                                                                                                                                        
                                scrollToSearchResult('%s', %s, %s);""",
                        cssClassUsedForHighlighting,
                        selectedSearchResultIndex,
                        scrollToNextResult);
    
                return (int) webEngine.executeScript(javascript);
            }
    
            String getDomAsHtmlString() {
                final String javascript = "document.documentElement.outerHTML";
                return (String) webEngine.executeScript(javascript);
            }
    
        }
    
    }