org.waveprotocol.wave.client.wavepanel.view.dom.DomUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.waveprotocol.wave.client.wavepanel.view.dom.DomUtil.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.waveprotocol.wave.client.wavepanel.view.dom;

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style;
import com.google.gwt.user.client.DOM;

import org.waveprotocol.wave.client.common.util.MeasurerInstance;
import org.waveprotocol.wave.client.debug.logger.DomLogger;
import org.waveprotocol.wave.client.uibuilder.BuilderHelper;
import org.waveprotocol.wave.client.wavepanel.view.View.Type;
import org.waveprotocol.wave.client.wavepanel.view.dom.full.TypeCodes;
import org.waveprotocol.wave.common.logging.LoggerBundle;
import org.waveprotocol.wave.model.util.Interval;
import org.waveprotocol.wave.model.util.Rect;

/**
 * Utilities for working with DOM objects.
 */
public class DomUtil {

    protected static LoggerBundle LOG = new DomLogger("render");

    private static Boolean isTouchDevice = null;

    //
    // Constants
    //

    /** Main element id. */
    private static final String MAIN_ELEMENT_ID = "main";

    /** Quasi-deleted element boolean attribute. */
    private static final String DELETED_ATTRIBUTE = "deleted";

    /** The "cursor" property of the element style. */
    private static final String STYLE_CURSOR_PROPERTY = "cursor";

    /** The "min-height" property of the element style. */
    private static final String STYLE_MIN_HEIGHT_PROPERTY = "minHeight";

    /** The "max-height" property of the element style. */
    private static final String STYLE_MAX_HEIGHT_PROPERTY = "maxHeight";

    /** The default (arrow-shaped) cursor. */
    private static final String STYLE_CURSOR_DEFAULT_VALUE = "default";

    /** The progress (arrow and glass-shaped) cursor. */
    private static final String STYLE_CURSOR_PROGRESS_VALUE = "progress";

    /** String represenation of the boolean true value. */
    private static final String BOOLEAN_TRUE_VALUE = Boolean.toString(true);

    //
    // Public methods
    //

    /**
     * Adds or removes class name to/from the element.
     * 
     * @param element
     * @param className
     * @param toAdd true - if class name should be added; false - if removed
     */
    public static void addOrRemoveClassName(Element element, String className, boolean toAdd) {
        if (toAdd) {
            element.addClassName(className);
        } else {
            element.removeClassName(className);
        }
    }

    /**
     * Sets boolean attribute to the element.
     * 
     * @param element
     * @param attribute attribute name
     * @param value the boolean value to be set to the attribute; if false, the attribute is removed
     */
    public static void setElementBooleanAttribute(Element element, String attribute, boolean value) {
        if (element != null && attribute != null) {
            if (value) {
                element.setAttribute(attribute, BOOLEAN_TRUE_VALUE);
            } else {
                element.removeAttribute(attribute);
            }
        }
    }

    /**
     * Gets boolean value from the element.
     * 
     * @param element
     * @param attribute attribute name
     * @return true, if the attribute is found
     */
    public static boolean getElementBooleanAttribute(Element element, String attribute) {
        return element != null && attribute != null && element.hasAttribute(attribute);
    }

    /**
     * Sets quasi-deletion attribute to the element.
     * 
     * @param element 
     */
    public static void setQuasiDeleted(Element element) {
        setElementBooleanAttribute(element, DELETED_ATTRIBUTE, true);
    }

    /**
     * Checks quas-deletion attribute on the element and its parents.
     * 
     * @param element
     * @return true, if the element or some its parent has quasi-deletion attribute
     */
    public static boolean isQuasiDeleted(Element element) {
        while (element != null) {
            if (getElementBooleanAttribute(element, DELETED_ATTRIBUTE)) {
                return true;
            }
            element = element.getParentElement();
        }
        return false;
    }

    /**
     * Gets element type.
     * 
     * @param element
     * @return element type
     */
    public static Type getElementType(Element element) {
        String typeString = getElementTypeString(element);
        if (typeString == null) {
            return null;
        }
        return TypeCodes.type(typeString);
    }

    /**
     * Sets element type.
     * 
     * @param element
     * @param type 
     */
    public static void setElementType(Element element, Type type) {
        if (element != null) {
            element.setAttribute(BuilderHelper.KIND_ATTRIBUTE, TypeCodes.kind(type));
        }
    }

    /**
     * @return true, if the element has the specified type.
     * 
     * @param element
     * @param type
     */
    public static boolean doesElementHaveType(Element element, Type type) {
        return doesElementHaveTypeString(element, TypeCodes.kind(type));
    }

    /**
     * Finds parent element with the specified type, starting with given element.
     * 
     * @param startElement
     * @param parentType
     * @return found parent element, or null, if it doesn't exist
     */
    public static Element findParentElement(Element startElement, Type parentType) {
        Element element = startElement;
        if (element != null) {
            String parentTypeString = TypeCodes.kind(parentType);
            element = element.getParentElement();
            while (element != null && !doesElementHaveTypeString(element, parentTypeString)) {
                element = element.getParentElement();
            }
        }
        return element;
    }

    public static Element findFirstChildElement(Element parent, Type childType) {
        return findChildElementExclusive(parent, TypeCodes.kind(childType), null, null, +1);
    }

    public static Element findFirstChildElement(Element parent, Type... childTypes) {
        Element element = parent;
        for (Type childType : childTypes) {
            element = findChildElementExclusive(element, TypeCodes.kind(childType), null, null, +1);
        }
        return element;
    }

    public static Element findFirstSiblingElement(Element element, Type siblingType) {
        if (element != null) {
            return findChildElementExclusive(element.getParentElement(), TypeCodes.kind(siblingType), element, null,
                    +1);
        }
        return null;
    }

    public static void setMainElement(Element element) {
        element.setId(MAIN_ELEMENT_ID);
    }

    /**
     * @return element with "main" id.
     */
    public static Element getMainElement() {
        return Document.get().getElementById(MAIN_ELEMENT_ID);
    }

    /**
     * @return element containing elements for blips which don't have inline parents.
     */
    public static Element getRootBlipContainer() {
        return findFirstChildElement(getMainElement(), Type.ROOT_CONVERSATION, Type.SCROLL_PANEL, Type.ROOT_THREAD,
                Type.BLIPS);
    }

    /**
     * @return height of the given element in pixels.
     * 
     * @param element the given element
     */
    public static int getElementHeight(Element element) {
        return (int) MeasurerInstance.get().height(element);
    }

    /**
     * @return absolute coordinates of the element as a rectangle.
     */
    public static Rect getElementAbsoluteRect(Element element) {
        return new Rect(getElementAbsoluteHorizontalSize(element), getElementAbsoluteVerticalSize(element));
    }

    /**
     * @return absolute horizontal size of the element.
     */
    public static Interval getElementAbsoluteHorizontalSize(Element element) {
        return Interval.create(element.getAbsoluteLeft(), element.getAbsoluteRight());
    }

    /**
     * @return absolute vertical size of the element.
     */
    public static Interval getElementAbsoluteVerticalSize(Element element) {
        return Interval.create(element.getAbsoluteTop(), element.getAbsoluteBottom());
    }

    /**
     * Fixes element's height with the given value.
     */
    public static void fixElementHeight(Element element, int fixedHeight) {
        Style style = element.getStyle();
        style.setPropertyPx(STYLE_MIN_HEIGHT_PROPERTY, fixedHeight);
        style.setPropertyPx(STYLE_MAX_HEIGHT_PROPERTY, fixedHeight);
    }

    /**
     * Unfixes element's height.
     */
    public static void unfixElementHeight(Element element) {
        Style style = element.getStyle();
        style.clearProperty(STYLE_MIN_HEIGHT_PROPERTY);
        style.clearProperty(STYLE_MAX_HEIGHT_PROPERTY);
    }

    /**
     * Sets default (arrow-shaped) cursor to the element.
     */
    public static void setDefaultCursor(Element element) {
        setCursor(element, STYLE_CURSOR_DEFAULT_VALUE);
    }

    /**
     * Sets progress (arrow with glass-shaped) cursor to the element.
     */
    public static void setProgressCursor(Element element) {
        setCursor(element, STYLE_CURSOR_PROGRESS_VALUE);
    }

    /**
     * Puts cover element on the covered element.
     * The cover and covered elements have the same position and size.
     * 
     * @param cover the cover element
     * @param covered the covered element
     */
    public static void putCover(Element cover, Element covered) {
        Element parent = covered.getParentElement();
        Rect coveredRect = DomUtil.getElementAbsoluteRect(covered);
        Rect parentRect = DomUtil.getElementAbsoluteRect(parent);
        Style coverStyle = cover.getStyle();
        coverStyle.setPosition(Style.Position.ABSOLUTE);
        coverStyle.setTop(coveredRect.getTop() - parentRect.getTop(), Style.Unit.PX);
        coverStyle.setLeft(coveredRect.getLeft() - parentRect.getLeft(), Style.Unit.PX);
        coverStyle.setRight(parentRect.getRight() - coveredRect.getRight(), Style.Unit.PX);
        coverStyle.setBottom(parentRect.getBottom() - coveredRect.getBottom(), Style.Unit.PX);
        parent.appendChild(cover);
    }

    /**
     * @return integer extracted from the string representation containing non-digit symbols
     */
    public static int extractInteger(String strValue) {
        String src = strValue.trim();
        String s = "";
        for (int i = 0; i < src.length(); i++) {
            char ch = src.charAt(i);
            if (ch >= '0' && ch <= '9') {
                s += src.substring(i, i + 1);
            }
        }
        return s.length() > 0 ? Integer.parseInt(s) : 0;
    }

    /**
     * Gets parent element on the given level.
     * 
     * @param startElement element to start with
     * @param level level of the parent to be found: 1 => parent, 2 => grandparent, and so on)
     * @return requested parent, or null, if it doesn't exist
     */
    public static Element getParentElement(Element startElement, int level) {
        Element e = startElement;
        for (int i = 0; i < level && e != null; i++) {
            e = e.getParentElement();
        }
        return e;
    }

    /**
     * @return visible child containing given vertical position using binary search.
     * @param parentElement parent element
     * @param y given vertical position
     */
    public static Element findChildElementContainingY(Element parentElement, int y) {
        if (parentElement.getAbsoluteTop() > y || parentElement.getAbsoluteBottom() < y) {
            return null;
        }
        int beginIndex = 0;
        int endIndex = DOM.getChildCount(parentElement) - 1;
        while (endIndex >= beginIndex) {
            int middleIndex = beginIndex + (endIndex - beginIndex) / 2;
            Element element = DOM.getChild(parentElement, middleIndex);
            int elementTop = element.getAbsoluteTop();
            if (elementTop > y) {
                endIndex = middleIndex - 1;
                continue;
            }
            int elementBottom = element.getAbsoluteBottom();
            if (elementBottom < y) {
                beginIndex = middleIndex + 1;
                continue;
            }
            return element;
        }
        return null;
    }

    /**
     * @return true, if the client device supports a touch screen.
     */
    public static boolean isTouchDevice() {
        if (isTouchDevice == null) {
            isTouchDevice = isTouchDeviceNative();

            if (LOG.trace().shouldLog()) {
                LOG.trace().log("DomUtil.isTouchDevice(): " + isTouchDevice);
            }
        }
        return isTouchDevice;
    }

    //
    // Private methods
    //

    /**
     * Gets string representation of the element type
     * 
     * @param element
     * @return string representation of the element type
     */
    private static String getElementTypeString(Element element) {
        if (element == null || !element.hasAttribute(BuilderHelper.KIND_ATTRIBUTE)) {
            return null;
        }
        return element.getAttribute(BuilderHelper.KIND_ATTRIBUTE);
    }

    /**
     * Returns true, if the element has given type's string representation.
     * 
     * @param element
     * @param typeString
     */
    private static boolean doesElementHaveTypeString(Element element, String typeString) {
        if (element == null) {
            return false;
        }
        if (typeString == null) {
            return true;
        }
        return typeString.equals(getElementTypeString(element));
    }

    /**
     * Returns the specified child element of the given element.
     * 
     * @param parent parent element
     * @param childTypeString string representaton of the child element
     * @param toExclude element to be excluded from the search
     * @param startFrom element to start search with
     * @param step step to change the child index
     */
    private static Element findChildElementExclusive(Element parent, String childTypeString, Element toExclude,
            Element startFrom, int step) {
        if (parent != null) {
            NodeList<Node> children = parent.getChildNodes();
            int begin;
            int end;
            if (step == +1) {
                begin = 0;
                end = children.getLength();
            } else {
                begin = children.getLength() - 1;
                end = -1;
            }
            boolean startFound = false;
            for (int i = begin; i != end; i += step) {
                Node child = children.getItem(i);
                if (child instanceof Element) {
                    Element e = (Element) child;
                    if (doesElementHaveTypeString(e, childTypeString)) {
                        if (e != toExclude) {
                            if (startFrom != null && !startFound) {
                                startFound = true;
                            } else {
                                return e;
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    /** 
     * Sets specified cursor to the element.
     * 
     * @param element
     * @param cursorName name of the cursor
     * 
     * Walkaround of Chrome bug when cursor on the element needs mouse move to be actually changed.
     * http://code.google.com/p/chromium/issues/detail?id=26723#c87
     */
    private static void setCursor(Element element, String cursorName) {
        String currentCursorName = element.getStyle().getCursor();
        if (!currentCursorName.equals(cursorName)) {
            Element wkch = Document.get().createDivElement();
            com.google.gwt.dom.client.Style wkchStyle = wkch.getStyle();
            wkchStyle.setOverflow(Style.Overflow.HIDDEN);
            wkchStyle.setPosition(Style.Position.ABSOLUTE);
            wkchStyle.setLeft(0, Style.Unit.PX);
            wkchStyle.setTop(0, Style.Unit.PX);
            wkchStyle.setWidth(100, Style.Unit.PCT);
            wkchStyle.setHeight(100, Style.Unit.PCT);

            Element wkch2 = Document.get().createDivElement();
            com.google.gwt.dom.client.Style wkch2Style = wkch2.getStyle();
            wkch2Style.setWidth(200, Style.Unit.PCT);
            wkch2Style.setHeight(200, Style.Unit.PCT);
            wkch.appendChild(wkch2);

            element.appendChild(wkch);
            element.getStyle().setProperty(STYLE_CURSOR_PROPERTY, cursorName);
            wkch.setScrollLeft(1);
            wkch.setScrollLeft(0);
            element.removeChild(wkch);
        }
    }

    /**
     * @return true, if the client device supports a touch screen.
     * 
     * See http://stackoverflow.com/a/14283643
     */
    private static native boolean isTouchDeviceNative() /*-{
                                                        var isTouchDevice = function() {  return 'ontouchstart' in window || 'onmsgesturechange' in window; };
                                                        return window.screenX == 0 || isTouchDevice() ? true : false;
                                                        }-*/;
}