org.rstudio.core.client.dom.DomUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.core.client.dom.DomUtils.java

Source

/*
 * DomUtils.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.core.client.dom;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayMixed;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.*;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.*;
import com.google.gwt.user.client.ui.UIObject;

import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.Point;
import org.rstudio.core.client.Rectangle;
import org.rstudio.core.client.dom.impl.DomUtilsImpl;
import org.rstudio.core.client.dom.impl.NodeRelativePosition;
import org.rstudio.core.client.regex.Match;
import org.rstudio.core.client.regex.Pattern;

/**
 * Helper methods that are mostly useful for interacting with 
 * contentEditable regions.
 */
public class DomUtils {
    public interface NodePredicate {
        boolean test(Node n);
    }

    public static native Element getActiveElement() /*-{
                                                    return $doc.activeElement;
                                                    }-*/;

    /**
     * In IE8, focusing the history table (which is larger than the scroll
     * panel it's contained in) causes the scroll panel to jump to the top.
     * Using setActive() solves this problem. Other browsers don't support
     * setActive but also don't have the scrolling problem.
     * @param element
     */
    public static native void setActive(Element element) /*-{
                                                         if (element.setActive)
                                                         element.setActive();
                                                         else
                                                         element.focus();
                                                         }-*/;

    public static int trimLines(Element element, int linesToTrim) {
        return trimLines(element.getChildNodes(), linesToTrim);
    }

    public static native void scrollToBottom(Element element) /*-{
                                                              element.scrollTop = element.scrollHeight;
                                                              }-*/;

    public static JavaScriptObject splice(JavaScriptObject array, int index, int howMany, String... elements) {
        JsArrayMixed args = JavaScriptObject.createArray().cast();
        args.push(index);
        args.push(howMany);
        for (String el : elements)
            args.push(el);
        return spliceInternal(array, args);
    }

    private static native JsArrayString spliceInternal(JavaScriptObject array, JsArrayMixed args) /*-{
                                                                                                  return Array.prototype.splice.apply(array, args);
                                                                                                  }-*/;

    public static Node findNodeUpwards(Node node, Element scope, NodePredicate predicate) {
        if (scope != null && !scope.isOrHasChild(node))
            throw new IllegalArgumentException("Incorrect scope passed to findParentNode");

        for (; node != null; node = node.getParentNode()) {
            if (predicate.test(node))
                return node;
            if (scope == node)
                return null;
        }
        return null;
    }

    public static boolean isEffectivelyVisible(Element element) {
        while (element != null) {
            if (!UIObject.isVisible(element))
                return false;

            // If element never equals body, then the element is not attached
            if (element == Document.get().getBody())
                return true;

            element = element.getParentElement();
        }

        // Element is not attached
        return false;
    }

    public static void selectElement(Element el) {
        impl.selectElement(el);
    }

    private static final Pattern NEWLINE = Pattern.create("\\n");

    private static int trimLines(NodeList<Node> nodes, final int linesToTrim) {
        if (nodes == null || nodes.getLength() == 0 || linesToTrim == 0)
            return 0;

        int linesLeft = linesToTrim;

        Node node = nodes.getItem(0);

        while (node != null && linesLeft > 0) {
            switch (node.getNodeType()) {
            case Node.ELEMENT_NODE:
                if (((Element) node).getTagName().equalsIgnoreCase("br")) {
                    linesLeft--;
                    node = removeAndGetNext(node);
                    continue;
                } else {
                    int trimmed = trimLines(node.getChildNodes(), linesLeft);
                    linesLeft -= trimmed;
                    if (!node.hasChildNodes())
                        node = removeAndGetNext(node);
                    continue;
                }
            case Node.TEXT_NODE:
                String text = ((Text) node).getData();

                Match lastMatch = null;
                Match match = NEWLINE.match(text, 0);
                while (match != null && linesLeft > 0) {
                    lastMatch = match;
                    linesLeft--;
                    match = match.nextMatch();
                }

                if (linesLeft > 0 || lastMatch == null) {
                    node = removeAndGetNext(node);
                    continue;
                } else {
                    int index = lastMatch.getIndex() + 1;
                    if (text.length() == index)
                        node.removeFromParent();
                    else
                        ((Text) node).deleteData(0, index);
                    break;
                }
            }
        }

        return linesToTrim - linesLeft;
    }

    private static Node removeAndGetNext(Node node) {
        Node next = node.getNextSibling();
        node.removeFromParent();
        return next;
    }

    /**
     *
     * @param node
     * @param pre Count hard returns in text nodes as newlines (only true if
     *    white-space mode is pre*)
     * @return
     */
    public static int countLines(Node node, boolean pre) {
        switch (node.getNodeType()) {
        case Node.TEXT_NODE:
            return countLinesInternal((Text) node, pre);
        case Node.ELEMENT_NODE:
            return countLinesInternal((Element) node, pre);
        default:
            return 0;
        }
    }

    private static int countLinesInternal(Text textNode, boolean pre) {
        if (!pre)
            return 0;
        String value = textNode.getData();
        Pattern pattern = Pattern.create("\\n");
        int count = 0;
        Match m = pattern.match(value, 0);
        while (m != null) {
            count++;
            m = m.nextMatch();
        }
        return count;
    }

    private static int countLinesInternal(Element elementNode, boolean pre) {
        if (elementNode.getTagName().equalsIgnoreCase("br"))
            return 1;

        int result = 0;
        NodeList<Node> children = elementNode.getChildNodes();
        for (int i = 0; i < children.getLength(); i++)
            result += countLines(children.getItem(i), pre);
        return result;
    }

    private final static DomUtilsImpl impl = GWT.create(DomUtilsImpl.class);

    /**
     * Drives focus to the element, and if (the element is contentEditable and
     * contains no text) or alwaysDriveSelection is true, also drives window
     * selection to the contents of the element. This is sometimes necessary
     * to get focus to move at all.
     */
    public static void focus(Element element, boolean alwaysDriveSelection) {
        impl.focus(element, alwaysDriveSelection);
    }

    public static native boolean hasFocus(Element element) /*-{
                                                           return element === $doc.activeElement;
                                                           }-*/;

    public static void collapseSelection(boolean toStart) {
        impl.collapseSelection(toStart);
    }

    public static boolean isSelectionCollapsed() {
        return impl.isSelectionCollapsed();
    }

    public static boolean isSelectionInElement(Element element) {
        return impl.isSelectionInElement(element);
    }

    /**
     * Returns true if the window contains an active selection.
     */
    public static boolean selectionExists() {
        return impl.selectionExists();
    }

    public static boolean contains(Element container, Node descendant) {
        while (descendant != null) {
            if (descendant == container)
                return true;

            descendant = descendant.getParentNode();
        }
        return false;
    }

    /**
     * CharacterData.deleteData(node, index, offset)
     */
    public static final native void deleteTextData(Text node, int offset, int length) /*-{
                                                                                      node.deleteData(offset, length);
                                                                                      }-*/;

    public static native void insertTextData(Text node, int offset, String data) /*-{
                                                                                 node.insertData(offset, data);
                                                                                 }-*/;

    public static Rectangle getCursorBounds() {
        return getCursorBounds(Document.get());
    }

    public static Rectangle getCursorBounds(Document doc) {
        return impl.getCursorBounds(doc);
    }

    public static String replaceSelection(Document document, String text) {
        return impl.replaceSelection(document, text);
    }

    public static String getSelectionText(Document document) {
        return impl.getSelectionText(document);
    }

    public static int[] getSelectionOffsets(Element container) {
        return impl.getSelectionOffsets(container);
    }

    public static void setSelectionOffsets(Element container, int start, int end) {
        impl.setSelectionOffsets(container, start, end);
    }

    public static Text splitTextNodeAt(Element container, int offset) {
        NodeRelativePosition pos = NodeRelativePosition.toPosition(container, offset);

        if (pos != null) {
            return ((Text) pos.node).splitText(pos.offset);
        } else {
            Text newNode = container.getOwnerDocument().createTextNode("");
            container.appendChild(newNode);
            return newNode;
        }
    }

    public static native Element getTableCell(Element table, int row, int col) /*-{
                                                                               return table.rows[row].cells[col] ;
                                                                               }-*/;

    public static void dump(Node node, String label) {
        StringBuffer buffer = new StringBuffer();
        dump(node, "", buffer, false);
        Debug.log("Dumping " + label + ":\n\n" + buffer.toString());
    }

    private static void dump(Node node, String indent, StringBuffer out, boolean doSiblings) {
        if (node == null)
            return;

        out.append(indent).append(node.getNodeName());
        if (node.getNodeType() != 1) {
            out.append(": \"").append(node.getNodeValue()).append("\"");
        }
        out.append("\n");

        dump(node.getFirstChild(), indent + "\u00A0\u00A0", out, true);
        if (doSiblings)
            dump(node.getNextSibling(), indent, out, true);
    }

    public static native void ensureVisibleVert(Element container, Element child, int padding) /*-{
                                                                                               if (!child)
                                                                                               return;
                                                                                                   
                                                                                               var height = child.offsetHeight ;
                                                                                               var top = 0;
                                                                                               while (child && child != container)
                                                                                               {
                                                                                               top += child.offsetTop ;
                                                                                               child = child.offsetParent ;
                                                                                               }
                                                                                                   
                                                                                               if (!child)
                                                                                               return;
                                                                                                   
                                                                                               // padding
                                                                                               top -= padding;
                                                                                               height += padding*2;
                                                                                                   
                                                                                               if (top < container.scrollTop)
                                                                                               {
                                                                                               container.scrollTop = top ;
                                                                                               }
                                                                                               else if (container.scrollTop + container.offsetHeight < top + height)
                                                                                               {
                                                                                               container.scrollTop = top + height - container.offsetHeight ;
                                                                                               }
                                                                                               }-*/;

    // Forked from com.google.gwt.dom.client.Element.scrollIntoView()
    public static native void scrollIntoViewVert(Element elem) /*-{
                                                               var top = elem.offsetTop;
                                                               var height = elem.offsetHeight;
                                                                   
                                                               if (elem.parentNode != elem.offsetParent) {
                                                               top -= elem.parentNode.offsetTop;
                                                               }
                                                                   
                                                               var cur = elem.parentNode;
                                                               while (cur && (cur.nodeType == 1)) {
                                                               if (top < cur.scrollTop) {
                                                               cur.scrollTop = top;
                                                               }
                                                               if (top + height > cur.scrollTop + cur.clientHeight) {
                                                               cur.scrollTop = (top + height) - cur.clientHeight;
                                                               }
                                                                   
                                                               var offsetTop = cur.offsetTop;
                                                               if (cur.parentNode != cur.offsetParent) {
                                                               offsetTop -= cur.parentNode.offsetTop;
                                                               }
                                                                   
                                                               top += offsetTop - cur.scrollTop;
                                                               cur = cur.parentNode;
                                                               }
                                                               }-*/;

    public static Point getRelativePosition(Element container, Element child) {
        int left = 0, top = 0;
        while (child != null && child != container) {
            left += child.getOffsetLeft();
            top += child.getOffsetTop();
            child = child.getOffsetParent();
        }

        return new Point(left, top);
    }

    public static int ensureVisibleHoriz(Element container, Element child, int paddingLeft, int paddingRight,
            boolean calculateOnly) {
        final int scrollLeft = container.getScrollLeft();

        if (child == null)
            return scrollLeft;

        int width = child.getOffsetWidth();
        int left = getRelativePosition(container, child).x;
        left -= paddingLeft;
        width += paddingLeft + paddingRight;

        int result;
        if (left < scrollLeft)
            result = left;
        else if (scrollLeft + container.getOffsetWidth() < left + width)
            result = left + width - container.getOffsetWidth();
        else
            result = scrollLeft;

        if (!calculateOnly && result != scrollLeft)
            container.setScrollLeft(result);

        return result;
    }

    public static native boolean isVisibleVert(Element container, Element child) /*-{
                                                                                 if (!container || !child)
                                                                                 return false;
                                                                                     
                                                                                 var height = child.offsetHeight;
                                                                                 var top = 0;
                                                                                 while (child && child != container)
                                                                                 {
                                                                                 top += child.offsetTop ;
                                                                                 child = child.offsetParent ;
                                                                                 }
                                                                                 if (!child)
                                                                                 throw new Error("Child was not in container or " +
                                                                                 "container wasn't offset parent");
                                                                                     
                                                                                 var bottom = top + height;
                                                                                 var scrollTop = container.scrollTop;
                                                                                 var scrollBottom = container.scrollTop + container.clientHeight;
                                                                                     
                                                                                 return (top > scrollTop && top < scrollBottom)
                                                                                 || (bottom > scrollTop && bottom < scrollBottom);
                                                                                     
                                                                                 }-*/;

    public static String getHtml(Node node) {
        switch (node.getNodeType()) {
        case Node.DOCUMENT_NODE:
            return ((ElementEx) node).getOuterHtml();
        case Node.ELEMENT_NODE:
            return ((ElementEx) node).getOuterHtml();
        case Node.TEXT_NODE:
            return node.getNodeValue();
        default:
            assert false : "Add case statement for node type " + node.getNodeType();
            return node.getNodeValue();
        }
    }

    public static boolean isDescendant(Node el, Node ancestor) {
        for (Node parent = el.getParentNode(); parent != null; parent = parent.getParentNode()) {
            if (parent.equals(ancestor))
                return true;
        }
        return false;
    }

    /**
     * Finds a node that matches the predicate.
     * 
     * @param start The node from which to start.
     * @param recursive If true, recurses into child nodes.
     * @param siblings If true, looks at the next sibling from "start".
     * @param filter The predicate that determines a match.
     * @return The first matching node encountered in documented order, or null.
     */
    public static Node findNode(Node start, boolean recursive, boolean siblings, NodePredicate filter) {
        if (start == null)
            return null;

        if (filter.test(start))
            return start;

        if (recursive) {
            Node result = findNode(start.getFirstChild(), true, true, filter);
            if (result != null)
                return result;
        }

        if (siblings) {
            Node result = findNode(start.getNextSibling(), recursive, true, filter);
            if (result != null)
                return result;
        }

        return null;
    }

    /**
     * Converts plaintext to HTML, preserving whitespace semantics
     * as much as possible.
     */
    public static String textToHtml(String text) {
        // Order of these replacement operations is important.
        return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
                .replaceAll("\\n", "<br />").replaceAll("\\t", "    ").replaceAll(" ", "&nbsp;")
                .replaceAll("&nbsp;(?!&nbsp;)", " ").replaceAll(" $", "&nbsp;").replaceAll("^ ", "&nbsp;");
    }

    public static String textToPreHtml(String text) {
        // Order of these replacement operations is important.
        return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\\t",
                "  ");
    }

    public static String htmlToText(String html) {
        Element el = DOM.createSpan();
        el.setInnerHTML(html);
        return el.getInnerText();
    }

    /**
     * Similar to Element.getInnerText() but converts br tags to newlines.
     */
    public static String getInnerText(Element el) {
        return getInnerText(el, false);
    }

    public static String getInnerText(Element el, boolean pasteMode) {
        StringBuilder out = new StringBuilder();
        getInnerText(el, out, pasteMode);
        return out.toString();
    }

    private static void getInnerText(Node node, StringBuilder out, boolean pasteMode) {
        if (node == null)
            return;

        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
            switch (child.getNodeType()) {
            case Node.TEXT_NODE:
                out.append(child.getNodeValue());
                break;
            case Node.ELEMENT_NODE:
                Element childEl = (Element) child;
                String tag = childEl.getTagName().toLowerCase();
                // Sometimes when pasting text (e.g. from IntelliJ) into console
                // the line breaks turn into <br _moz_dirty="true"/> or whatever.
                // We want to keep them in those cases. But in other cases
                // the _moz_dirty breaks are just spurious.
                if (tag.equals("br") && (pasteMode || !childEl.hasAttribute("_moz_dirty")))
                    out.append("\n");
                else if (tag.equals("script") || tag.equals("style"))
                    continue;
                getInnerText(child, out, pasteMode);
                break;
            }
        }
    }

    public static void setInnerText(Element el, String plainText) {
        el.setInnerText("");
        if (plainText == null || plainText.length() == 0)
            return;

        Document doc = el.getOwnerDocument();

        Pattern pattern = Pattern.create("\\n");
        int tail = 0;
        Match match = pattern.match(plainText, 0);
        while (match != null) {
            if (tail != match.getIndex()) {
                String line = plainText.substring(tail, match.getIndex());
                el.appendChild(doc.createTextNode(line));
            }
            el.appendChild(doc.createBRElement());
            tail = match.getIndex() + 1;
            match = match.nextMatch();
        }

        if (tail < plainText.length())
            el.appendChild(doc.createTextNode(plainText.substring(tail)));
    }

    public static boolean isSelectionAsynchronous() {
        return impl.isSelectionAsynchronous();
    }

    public static boolean isCommandClick(NativeEvent nativeEvt) {
        boolean commandKey;
        if (BrowseCap.isMacintosh()) {
            commandKey = nativeEvt.getMetaKey();
        } else {
            commandKey = nativeEvt.getCtrlKey();
        }

        return (nativeEvt.getButton() == NativeEvent.BUTTON_LEFT) && commandKey;
    }

    // Returns the relative vertical position of a child to its parent. 
    // Presumes that the parent is one of the elements from which the child's
    // position is computed; if this is not the case, the child's position
    // relative to the body is returned.
    public static int topRelativeTo(Element parent, Element child) {
        int top = 0;
        Element el = child;
        while (el != null && el != parent) {
            top += el.getOffsetTop();
            el = el.getOffsetParent();
        }
        return top;
    }

    public static int bottomRelativeTo(Element parent, Element child) {
        return topRelativeTo(parent, child) + child.getOffsetHeight();
    }

    public static int leftRelativeTo(Element parent, Element child) {
        int left = 0;
        Element el = child;
        while (el != null && el != parent) {
            left += el.getOffsetLeft();
            el = el.getOffsetParent();
        }
        return left;
    }

    public static final native void setStyle(Element element, String name, String value) /*-{
                                                                                         element.style[name] = value;
                                                                                         }-*/;
}