/*
* Copyright 2000-2018 Vaadin Ltd.
*
* Licensed 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 com.vaadin.client;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
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.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.KeyEvent;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.shared.ui.ErrorLevel;
import com.vaadin.shared.util.SharedUtil;
/**
* Utility methods which are related to client side code only
*/
public class WidgetUtil {
/**
* Helper method for debugging purposes.
*
* Stops execution on firefox browsers on a breakpoint.
*
*/
public static native void browserDebugger()
/*-{
if($wnd.console)
debugger;
}-*/;
/**
* Redirects the browser to the given url or refreshes the page if url is
* null
*
* @since 7.6
* @param url
* The url to redirect to or null to refresh
*/
public static native void redirect(String url)
/*-{
if (url) {
$wnd.location = url;
} else {
$wnd.location.reload(false);
}
}-*/;
/**
* Helper method for a bug fix #14041. For mozilla getKeyCode return 0 for
* space bar (because space is considered as char). If return 0 use
* getCharCode.
*
* @param event
* @return return key code
* @since 7.2.4
*/
public static int getKeyCode(KeyEvent> event) {
int keyCode = event.getNativeEvent().getKeyCode();
if (keyCode == 0) {
keyCode = event.getNativeEvent().getCharCode();
}
return keyCode;
}
/**
*
* Returns the topmost element of from given coordinates.
*
* TODO fix crossplat issues clientX vs pageX. See quircksmode. Not critical
* for vaadin as we scroll div istead of page.
*
* @param x
* @param y
* @return the element at given coordinates
*/
public static native Element getElementFromPoint(int clientX, int clientY)
/*-{
var el = $wnd.document.elementFromPoint(clientX, clientY);
// Call elementFromPoint two times to make sure IE8 also returns something sensible if the application is running in an iframe
el = $wnd.document.elementFromPoint(clientX, clientY);
if(el != null && el.nodeType == 3) {
el = el.parentNode;
}
return el;
}-*/;
public static float parseRelativeSize(String size) {
if (size == null || !size.endsWith("%")) {
return -1;
}
try {
return Float.parseFloat(size.substring(0, size.length() - 1));
} catch (Exception e) {
getLogger().warning("Unable to parse relative size");
return -1;
}
}
private static final Element escapeHtmlHelper = DOM.createDiv();
/**
* Converts html entities to text.
*
* @param html
* @return escaped string presentation of given html
*/
public static String escapeHTML(String html) {
DOM.setInnerText(escapeHtmlHelper, html);
String escapedText = DOM.getInnerHTML(escapeHtmlHelper);
if (BrowserInfo.get().isIE8()) {
// #7478 IE8 "incorrectly" returns "
" for newlines set using
// setInnerText. The same for " " which is converted to " "
escapedText = escapedText.replaceAll("<(BR|br)>", "\n");
escapedText = escapedText.replaceAll(" ", " ");
}
return escapedText;
}
/**
* Escapes the string so it is safe to write inside an HTML attribute.
*
* @param attribute
* The string to escape
* @return An escaped version of
* In case the browser doesn't support bounding rectangles, the returned * value is the offset width. * * @param element * the element of which to calculate the width * @return the width of the element */ public static int getRequiredWidthBoundingClientRect( com.google.gwt.dom.client.Element element) { return (int) Math .ceil(getRequiredWidthBoundingClientRectDouble(element)); } /** * Calculates the width of the element's bounding rectangle to subpixel * precision. *
* In case the browser doesn't support bounding rectangles, the returned * value is the offset width. * * @param element * the element of which to calculate the width * @return the subpixel-accurate width of the element * @since 7.4 */ public static native double getRequiredWidthBoundingClientRectDouble( com.google.gwt.dom.client.Element element) /*-{ if (element.getBoundingClientRect) { var rect = element.getBoundingClientRect(); return rect.right - rect.left; } else { return element.offsetWidth; } }-*/; public static int getRequiredHeightComputedStyle( com.google.gwt.dom.client.Element element) { return (int) Math.ceil(getRequiredHeightComputedStyleDouble(element)); } public static native double getRequiredHeightComputedStyleDouble( com.google.gwt.dom.client.Element element) /*-{ var cs = element.ownerDocument.defaultView.getComputedStyle(element); var heightPx = cs.height; if(heightPx == 'auto'){ // Fallback for inline elements return @com.vaadin.client.WidgetUtil::getRequiredHeightBoundingClientRectDouble(Lcom/google/gwt/dom/client/Element;)(element); } var height = parseFloat(heightPx); // Will automatically skip "px" suffix var border = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth); // Will automatically skip "px" suffix var padding = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom); // Will automatically skip "px" suffix return height+border+padding; }-*/; public static int getRequiredWidthComputedStyle( com.google.gwt.dom.client.Element element) { return (int) Math.ceil(getRequiredWidthComputedStyleDouble(element)); } public static native int getRequiredWidthComputedStyleDouble( com.google.gwt.dom.client.Element element) /*-{ var cs = element.ownerDocument.defaultView.getComputedStyle(element); var widthPx = cs.width; if(widthPx == 'auto'){ // Fallback for inline elements return @com.vaadin.client.WidgetUtil::getRequiredWidthBoundingClientRectDouble(Lcom/google/gwt/dom/client/Element;)(element); } var width = parseFloat(widthPx); // Will automatically skip "px" suffix var border = parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth); // Will automatically skip "px" suffix var padding = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight); // Will automatically skip "px" suffix return width+border+padding; }-*/; /** * Calculates the height of the element's bounding rectangle. *
* In case the browser doesn't support bounding rectangles, the returned * value is the offset height. * * @param element * the element of which to calculate the height * @return the height of the element */ public static int getRequiredHeightBoundingClientRect( com.google.gwt.dom.client.Element element) { return (int) Math .ceil(getRequiredHeightBoundingClientRectDouble(element)); } /** * Calculates the height of the element's bounding rectangle to subpixel * precision. *
* In case the browser doesn't support bounding rectangles, the returned * value is the offset height. * * @param element * the element of which to calculate the height * @return the subpixel-accurate height of the element * @since 7.4 */ public static native double getRequiredHeightBoundingClientRectDouble( com.google.gwt.dom.client.Element element) /*-{ var height; if (element.getBoundingClientRect != null) { var rect = element.getBoundingClientRect(); height = rect.bottom - rect.top; } else { height = element.offsetHeight; } return height; }-*/; public static int getRequiredWidth(Widget widget) { return getRequiredWidth(widget.getElement()); } public static int getRequiredHeight(Widget widget) { return getRequiredHeight(widget.getElement()); } /** * Detects what is currently the overflow style attribute in given element. * * @param pe * the element to detect * @return true if auto or scroll */ public static boolean mayHaveScrollBars( com.google.gwt.dom.client.Element pe) { String overflow = getComputedStyle(pe, "overflow"); if (overflow != null) { if (overflow.equals("auto") || overflow.equals("scroll")) { return true; } else { return false; } } else { return false; } } /** * A simple helper method to detect "computed style" (aka style sheets + * element styles). Values returned differ a lot depending on browsers. * Always be very careful when using this. * * @param el * the element from which the style property is detected * @param p * the property to detect * @return String value of style property */ private static native String getComputedStyle( com.google.gwt.dom.client.Element el, String p) /*-{ try { if (el.currentStyle) { // IE return el.currentStyle[p]; } else if (window.getComputedStyle) { // Sa, FF, Opera var view = el.ownerDocument.defaultView; return view.getComputedStyle(el,null).getPropertyValue(p); } else { // fall back for non IE, Sa, FF, Opera return ""; } } catch (e) { return ""; } }-*/; /** * Will (attempt) to focus the given DOM Element. * * @param el * the element to focus */ public static native void focus(Element el) /*-{ try { el.focus(); } catch (e) { } }-*/; /** * Helper method to find first instance of any Widget found by traversing * DOM upwards from given element. *
* Note: If {@code element} is inside some widget {@code W}
* , and {@code W} in turn is wrapped in a {@link Composite}
* {@code C}, this method will not find {@code W} but returns {@code C}.
* This may also be the case with other Composite-like classes that hijack
* the event handling of their child widget(s).
*
* @param element
* the element where to start seeking of Widget
* @since 7.7.11
*/
@SuppressWarnings("unchecked")
public static
* Note: If {@code element} is inside some widget {@code W}
* , and {@code W} in turn is wrapped in a {@link Composite}
* {@code C}, this method will not find {@code W}. It returns either
* {@code C} or null, depending on whether the class parameter matches. This
* may also be the case with other Composite-like classes that hijack the
* event handling of their child widget(s).
*
* Only accepts the exact class {@code class1} if not null.
*
* @param element
* the element where to start seeking of Widget
* @param class1
* the Widget type to seek for, null for any
*/
@SuppressWarnings("unchecked")
public static
* Note: If {@code element} is inside some widget {@code W}
* , and {@code W} in turn is wrapped in a {@link Composite} {@code
* C}, this method will not find {@code W}. It returns either {@code C} or
* null, depending on whether the class parameter matches. This may also be
* the case with other Composite-like classes that hijack the event handling
* of their child widget(s).
*
* @param element
* the element where to start seeking of Widget
* @param class1
* the Widget type to seek for
* @param exactMatch
* true to only accept class1, false to also accept its
* superclasses
* @since 7.7.11
*/
@SuppressWarnings("unchecked")
public static
* Since we're comparing pixels on a screen, epsilon must be less than 1.
* 0.49 was deemed a perfectly fine and beautifully round number.
*/
public static final double PIXEL_EPSILON = 0.49d;
/**
* Compares two double values with the error margin of
* {@link #PIXEL_EPSILON} (i.e. {@value #PIXEL_EPSILON})
*
* @param num1
* the first value for which to compare equality
* @param num2
* the second value for which to compare equality
* @since 7.4
*
* @return true if the values are considered equals; false otherwise
*/
public static boolean pixelValuesEqual(final double num1,
final double num2) {
return Math.abs(num1 - num2) <= PIXEL_EPSILON;
}
public static native TextRectangle getBoundingClientRect(Element e)
/*-{
return e.getBoundingClientRect();
}-*/;
public static final class TextRectangle extends JavaScriptObject {
protected TextRectangle() {
}
public native double getBottom()
/*-{
return this.bottom;
}-*/;
public native double getHeight()
/*-{
return this.height;
}-*/;
public native double getLeft()
/*-{
return this.left;
}-*/;
public native double getRight()
/*-{
return this.right;
}-*/;
public native double getTop()
/*-{
return this.top;
}-*/;
public native double getWidth()
/*-{
return this.width;
}-*/;
}
/**
* Wrap a css size value and its unit and translate back and forth to the
* string representation.
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since 7.5.0
* @param element
* the element to measure
* @return the top border thickness
*/
public static double getBorderTopThickness(Element element) {
return getBorderThickness(element, new String[] { "borderTopWidth" });
}
/**
* Returns the thickness of the given element's bottom border.
*
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since 7.5.0
* @param element
* the element to measure
* @return the bottom border thickness
*/
public static double getBorderBottomThickness(Element element) {
return getBorderThickness(element,
new String[] { "borderBottomWidth" });
}
/**
* Returns the combined thickness of the given element's top and bottom
* borders.
*
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since 7.5.0
* @param element
* the element to measure
* @return the top and bottom border thickness
*/
public static double getBorderTopAndBottomThickness(Element element) {
return getBorderThickness(element,
new String[] { "borderTopWidth", "borderBottomWidth" });
}
/**
* Returns the thickness of the given element's left border.
*
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since 7.5.0
* @param element
* the element to measure
* @return the left border thickness
*/
public static double getBorderLeftThickness(Element element) {
return getBorderThickness(element, new String[] { "borderLeftWidth" });
}
/**
* Returns the thickness of the given element's right border.
*
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since 7.5.0
* @param element
* the element to measure
* @return the right border thickness
*/
public static double getBorderRightThickness(Element element) {
return getBorderThickness(element, new String[] { "borderRightWidth" });
}
/**
* Returns the thickness of the given element's left and right borders.
*
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since 7.5.0
* @param element
* the element to measure
* @return the top border thickness
*/
public static double getBorderLeftAndRightThickness(Element element) {
return getBorderThickness(element,
new String[] { "borderLeftWidth", "borderRightWidth" });
}
private static native double getBorderThickness(
com.google.gwt.dom.client.Element element, String[] borderNames)
/*-{
if (typeof $wnd.getComputedStyle === 'function') {
var computedStyle = $wnd.getComputedStyle(element);
var width = 0;
for (i=0; i< borderNames.length; i++) {
var borderWidth = computedStyle[borderNames[i]];
width += parseFloat(borderWidth);
}
return width;
} else {
var parentElement = element.offsetParent;
var cloneElement = element.cloneNode(false);
cloneElement.style.boxSizing ="content-box";
parentElement.appendChild(cloneElement);
cloneElement.style.height = "10px"; // IE8 wants the height to be set to something...
var heightWithBorder = cloneElement.offsetHeight;
for (i=0; i< borderNames.length; i++) {
cloneElement.style[borderNames[i]] = "0";
}
var heightWithoutBorder = cloneElement.offsetHeight;
parentElement.removeChild(cloneElement);
return heightWithBorder - heightWithoutBorder;
}
}-*/;
/**
* Rounds the given size up to a value which the browser will accept.
*
* Safari/WebKit uses 1/64th of a pixel to enable using integer math
* (http://trac.webkit.org/wiki/LayoutUnit).
*
* Firefox uses 1/60th of a pixel because it is divisible by three
* (https://bugzilla.mozilla.org/show_bug.cgi?id=1070940)
*
* @since 7.5.1
* @param size
* the value to round
* @return the rounded value
*/
public static double roundSizeUp(double size) {
return roundSize(size, true);
}
/**
* Rounds the given size down to a value which the browser will accept.
*
* Safari/WebKit uses 1/64th of a pixel to enable using integer math
* (http://trac.webkit.org/wiki/LayoutUnit).
*
* Firefox uses 1/60th of a pixel because it is divisible by three
* (https://bugzilla.mozilla.org/show_bug.cgi?id=1070940)
*
* IE9+ uses 1/100th of a pixel
*
* @since 7.5.1
* @param size
* the value to round
* @return the rounded value
*/
public static double roundSizeDown(double size) {
return roundSize(size, false);
}
private static double roundSize(double size, boolean roundUp) {
if (BrowserInfo.get().isIE8()) {
if (roundUp) {
return Math.ceil(size);
} else {
return (int) size;
}
}
double factor = getSubPixelRoundingFactor();
if (factor < 0 || size < 0) {
return size;
}
if (roundUp) {
return roundSizeUp(size, factor);
} else {
return roundSizeDown(size, factor);
}
}
/**
* Returns the factor used by browsers to round subpixel values
*
* @since 7.5.1
* @return the factor N used by the browser when storing subpixels as X+Y/N
*/
private static double getSubPixelRoundingFactor() {
// Detects how the browser does subpixel rounding
// Currently Firefox uses 1/60th pixels
// and Safari uses 1/64th pixels
// IE 1/100th pixels
if (detectedSubPixelRoundingFactor != -1) {
return detectedSubPixelRoundingFactor;
}
double probeSize = 0.999999;
DivElement div = Document.get().createDivElement();
Document.get().getBody().appendChild(div);
div.getStyle().setHeight(probeSize, Unit.PX);
ComputedStyle computedStyle = new ComputedStyle(div);
double computedHeight = computedStyle.getHeight();
if (computedHeight < probeSize) {
// Rounded down by browser, all browsers but Firefox do this
// today
detectedSubPixelRoundingFactor = (int) Math
.round(1.0 / (1.0 - computedHeight));
} else {
// Rounded up / to nearest by browser
probeSize = 1;
while (computedStyle.getHeight() != 0.0) {
computedHeight = computedStyle.getHeight();
probeSize /= 2.0;
div.getStyle().setHeight(probeSize, Unit.PX);
}
detectedSubPixelRoundingFactor = (int) Math
.round(1.0 / computedHeight);
}
div.removeFromParent();
return detectedSubPixelRoundingFactor;
}
private static double roundSizeUp(double size, double divisor) {
// In: 12.51, 60.0
// 12
double integerPart = (int) size;
// (12.51 - 12) * 60 = 30.6
double nrFractions = (size - integerPart) * divisor;
// 12 + ceil(30.6) / 60 = 12 + 31/60 = 12.51666
return integerPart + (Math.ceil(nrFractions)) / divisor;
}
private static double roundSizeDown(double size, double divisor) {
// In: 12.51, 60.0
// 12
double integerPart = (int) size;
// (12.51 - 12) * 60 = 30.6
double nrFractions = (size - integerPart) * divisor;
// 12 + int(30.6) / 60 = 12 + 30/60 = 12.5
return integerPart + ((int) nrFractions) / divisor;
}
/**
* Returns whether the given element is displayed.
*
* This method returns false if either the given element or any of its
* ancestors has the style {@code display: none} applied.
*
* @param element
* the element to test for visibility
* @return {@code true} if the element is displayed, {@code false} otherwise
* @since 7.7.13
*/
public static native boolean isDisplayed(Element element)
/*-{
// This measurement is borrowed from JQuery and measures the visible
// size of the element. The measurement should return false when either
// the element or any of its ancestors has "display: none" style.
return !!(element.offsetWidth || element.offsetHeight
|| element.getClientRects().length);
}-*/;
/**
* Utility methods for displaying error message on components.
*
* @since 7.7.11
*/
public static class ErrorUtil {
/**
* Sets the error level style name for the given element and removes all
* previously applied error level style names. The style name has the
* {@code prefix-errorLevel} format.
*
* @param element
* element to apply the style name to
* @param prefix
* part of the style name before the error level string
* @param errorLevel
* error level for which the style will be applied
*/
public static void setErrorLevelStyle(Element element, String prefix,
ErrorLevel errorLevel) {
for (ErrorLevel errorLevelValue : ErrorLevel.values()) {
String className = prefix + "-"
+ errorLevelValue.toString().toLowerCase();
if (errorLevel == errorLevelValue) {
element.addClassName(className);
} else {
element.removeClassName(className);
}
}
}
}
}
true
if selection is enabled; false
* if not
*/
public native static void setTextSelectionEnabled(Element e, boolean enable)
/*-{
if (!enable) {
e.ondrag = function () { return false; };
e.onselectstart = function () { return false; };
e.style.webkitUserSelect = "none";
} else {
e.ondrag = null;
e.onselectstart = null;
e.style.webkitUserSelect = "text";
}
}-*/;
/**
* JavaScript hack to clear text selection in various browsers.
*
* @since 7.6
*/
public native static void clearTextSelection()
/*-{
if ($wnd.getSelection) {
$wnd.getSelection().removeAllRanges();
}
}-*/;
/**
* The allowed value inaccuracy when comparing two double-typed pixel
* values.
*
* Eg. 50%, 123px, ...
*
* @since 7.2.6
* @author Vaadin Ltd
*/
@SuppressWarnings("serial")
public static class CssSize implements Serializable {
/*
* Map the size units with their type.
*/
private static Map