package com.project.shared.client.utils; import java.util.ArrayList; import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.user.client.DOM; import com.project.shared.data.Rgba; import com.project.shared.utils.ArrayUtils; import com.project.shared.utils.StringUtils; public class StyleUtils { public static Integer getTopPx(Style style) { return fromPXUnitString(style.getTop()); } public static Integer getLeftPx(Style style) { return fromPXUnitString(style.getLeft()); } public static Integer getHeightPx(Style style) { return fromPXUnitString(style.getHeight()); } public static Integer getWidthPx(Style style) { return fromPXUnitString(style.getWidth()); } /** * @return True if the given style objects are equivalent - every property set in <code>a</code> is also set in <code>b</code> and has the same value, and vice verse. */ public static boolean areEquivalent(Style a, Style b) { return StyleUtils.isSubsetOf(a, b) && StyleUtils.isSubsetOf(b, a); } /** * @return True if two elements have equivalent computed styles (more accurately: "used styles"). False otherwise. * <p>Note: Having equivalent computed styles does not guarantee that they look the same on the screen - due to text-decoration's weird specification.</p> */ public static boolean areComputedStylesEquivalent(Element a, Element b) { return StyleUtils.areEquivalent(StyleUtils.getComputedStyle(a, null), StyleUtils.getComputedStyle(b, null)); } /** * @return True if the element has a style equivalent to it's direct parent. */ public static boolean hasCompletelyInheritedStyle(Element childElem) { return StyleUtils.areComputedStylesEquivalent(childElem, childElem.getParentElement()); } /** * Compares two styles, checking if one is a subset of the other. * @return true if every property of the "subsetCandidate" exists in set and has the same value. Otherwise, returns false. */ public static native final boolean isSubsetOf(Style subsetCandidate, Style set) /*-{ for (var i = 0; i < subsetCandidate.length; i++) { var name = subsetCandidate[i]; if (subsetCandidate.getPropertyValue(name) !== set.getPropertyValue(name)) { return false; } } return true; }-*/; /** * See https://developer.mozilla.org/en/DOM/window.getComputedStyle * <p><strong>Warning</strong>: for the text-decoration css property, consider using {@link #getInheritedTextDecoration(Element)}.</p> * @param elem The element for which to get the computed style object * @param pseudoElement can be null for most purposes, may work with ":after" or ":before" - I couldn't get it to work. * @return The "final" style of the element */ public static native final Style getComputedStyle(Element elem, String pseudoElement) /*-{ return $wnd.getComputedStyle(elem, pseudoElement); }-*/; /** * <p>This method is neccesary because getComputedStyle will not return what you expect in the case of text-decoration - because no matter what value * the element has in text-decoration, it will always actually be displayed with the top-most ancestor that sets this property.</p> * @see <a href="http://stackoverflow.com/questions/4481318/css-text-decoration-property-cannot-be-overridden-by-ancestor-element">Question in stackoverflow.com</a> * @param elem * @return The inherited value for the css text-decoration property. */ public static String getInheritedTextDecoration(Element elem) { if (null == elem) { return ""; } String value = elem.getStyle().getTextDecoration(); if (value.isEmpty()) { return StyleUtils.getInheritedTextDecoration(elem.getParentElement()); } return value; } /** * Returns the native <code>cssText</code> property of a style object. */ public static final native String getCssText(Style style) /*-{ return style.cssText; }-*/; public static native void setCssText(Style style, String cssText) /*-{ style.cssText = cssText; }-*/; /** * <p>Wraps all the text node descendants with span elements and moves all text-decoration * style declarations down into the text-node wrappers.</p> * <ul><li>If an element contains only text nodes, it does not wrap the text.</li> * <li>If any descendant of elem is an empty span, it removes it from the tree.</li></ul> * <p> * There reason we need this, is that there's a general problem with text-decoration, * that a child element can never override that value if a parent has set it.</p> * @see <a href="http://stackoverflow.com/questions/4481318/css-text-decoration-property-cannot-be-overridden-by-ancestor-element">Question in stackoverflow.com</a> * * @param elem */ public static void pushStylesDownToTextNodes(Element elem) { boolean needsWrappers = false; ArrayList<Node> childNodes = ElementUtils.getChildNodes(elem); for (Node childNode : childNodes) { if (Node.TEXT_NODE != childNode.getNodeType()) { needsWrappers = true; } } if (false == needsWrappers) { return; } for (Node childNode : childNodes) { if (Node.TEXT_NODE == childNode.getNodeType()) { Element newChild = DOM.createSpan(); StyleUtils.copyStyle(newChild, elem, false); newChild.setInnerText(childNode.getNodeValue()); elem.replaceChild(newChild, childNode); } else if (Node.ELEMENT_NODE == childNode.getNodeType()) { Element childElem = Element.as(childNode); if (ElementUtils.isSpanElement(childElem) && (false == childElem.hasChildNodes())) { // trim empty spans childElem.removeFromParent(); continue; } StyleUtils.copyStyle(childElem, elem, false); pushStylesDownToTextNodes(childElem); } // Ignore Node.DOCUMENT_NODE } StyleUtils.deleteStyle(elem); } /** * Copies the element style (not computed style!) from one element to another. * @param to * @param from * @param overrideExistingPropertiesInTarget * <ul><li>True - will override the target element's style completely</li> * <li>False - will only copy those css properties which are set on the source, but not set on the target.</li></ul> */ public static native final void copyStyle(Element to, Element from, boolean overrideExistingPropertiesInTarget) /*-{ if (overrideExistingPropertiesInTarget) { to.style.cssText = from.style.cssText; return; } var existingPropertiesInTarget = []; for (var i = 0; i < from.style.length; i++) { var name = from.style[i]; var existsInTarget = false; for (var j = 0; j < to.style.length; j++) { if (name === to.style[j]) { existsInTarget = true; break; } } if (existsInTarget) { continue; } to.style.setProperty(name, from.style.getPropertyValue(name), from.style.getPropertyPriority(name)); } }-*/; public static native final void deleteStyle(Element target) /*-{ target.style.cssText = ''; }-*/; public static final String buildBackgroundUrl(String imageUrl) { return "url(\"" + imageUrl + "\")"; } public static final String getBackgroundUrl(Style style) { String backgroundImage = style.getBackgroundImage(); if (Strings.isNullOrEmpty(backgroundImage)) { return ""; } if (backgroundImage.contains("url(")) { return backgroundImage.substring("url(\"".length(), backgroundImage.length() - "\")".length()); } else { return backgroundImage; } } public static void clearBackground(Style style) { style.clearProperty(CssProperties.BACKGROUND); style.clearProperty(CssProperties.BACKGROUND_REPEAT); style.clearProperty(CssProperties.BACKGROUND_POSITION_X); style.clearProperty(CssProperties.BACKGROUND_POSITION_Y); style.clearBackgroundColor(); style.clearBackgroundImage(); } public static void clearBackgroundRepeat(Style style) { style.clearProperty(CssProperties.BACKGROUND_REPEAT); } public static void setBackgroundRepeat(Style style, boolean repeat) { if (repeat) { style.setProperty(CssProperties.BACKGROUND_REPEAT, "repeat"); } else { style.setProperty(CssProperties.BACKGROUND_REPEAT, "no-repeat"); } } public static void clearBackgroundPosition(Style style) { style.clearProperty(CssProperties.BACKGROUND_POSITION_X); style.clearProperty(CssProperties.BACKGROUND_POSITION_Y); } public static void setBackgroundPositionX(Style style, int value) { style.setProperty(CssProperties.BACKGROUND_POSITION_X, String.valueOf(value) + "px"); } public static void setBackgroundPositionY(Style style, int value) { style.setProperty(CssProperties.BACKGROUND_POSITION_Y, String.valueOf(value) + "px"); } public static void setBackgroundCenterX(Style style) { style.setProperty(CssProperties.BACKGROUND_POSITION_X, "center"); } public static void setBackgroundCenterY(Style style) { style.setProperty(CssProperties.BACKGROUND_POSITION_Y, "center"); } public static void clearBackgroundSize(Style style) { style.clearProperty(CssProperties.BACKGROUND_SIZE); } public static void setBackgroundStretch(Style style, boolean stretchWidth, boolean stretchHeight) { String width = ""; if (stretchWidth){ width = "100%"; } else{ width = "auto"; } String height = ""; if (stretchHeight){ height = "100%"; } else{ height = "auto"; } style.setProperty(CssProperties.BACKGROUND_SIZE, width + " " + height); } static void setPropertyForAllVendors(Style style, String property, String value) { String propertyCapitalized = property.substring(0, 1).toUpperCase() + property.substring(1); style.setProperty(property, value); style.setProperty("Moz" + propertyCapitalized, value); style.setProperty("Webkit" + propertyCapitalized, value); style.setProperty("Khtml" + propertyCapitalized, value); style.setProperty("O" + propertyCapitalized, value); style.setProperty("Ms" + propertyCapitalized, value); StyleUtils.cssSetMSProperty(style, StringUtils.splitCamelCase(property, "-", true), value); } static void clearPropertyForAllVendors(Style style, String property) { String propertyCapitalized = property.substring(0, 1).toUpperCase() + property.substring(1); style.clearProperty(property); style.clearProperty("Moz" + propertyCapitalized); style.clearProperty("Webkit" + propertyCapitalized); style.clearProperty("Khtml" + propertyCapitalized); style.clearProperty("O" + propertyCapitalized); style.clearProperty("Ms" + propertyCapitalized); StyleUtils.cssClearMSProperty(style, StringUtils.splitCamelCase(property, "-", true)); } private static final native void cssSetMSProperty(Style style, String name, String value) /*-{ style['-ms-' + name] = value; }-*/; private static final native void cssClearMSProperty(Style style, String name) /*-{ style['-ms-' + name] = ""; }-*/; public enum UserSelectionMode { None, Text, @Deprecated /** Badly supported */ Toggle, @Deprecated /** Badly supported */ Element, @Deprecated /** Badly supported */ Elements, All, @Deprecated /** Badly supported */ Inherit } /** * Sets the user-select css property. * @See <a href="http://www.w3.org/TR/2000/WD-css3-userint-20000216.html#user-select">CSS 3 Spec.</a> */ public static void setUserSelectionMode(Style style, UserSelectionMode mode) { setPropertyForAllVendors(style, "userSelect", mode.name().toLowerCase()); if (UserSelectionMode.None == mode) { // see https://developer.mozilla.org/en/CSS/-moz-user-select style.setProperty("MozUserSelect", "-moz-none"); } } public static Integer fromPXUnitString(String cssPxUnitNumStr) { String PX_SUFFIX = "px"; String trimmedStr = cssPxUnitNumStr.trim(); if (false == trimmedStr.toLowerCase().endsWith(PX_SUFFIX)) { return null; } try { Double value = Double.valueOf(trimmedStr.substring(0, trimmedStr.length() - PX_SUFFIX.length())); return (int)Math.round(value); } catch (NumberFormatException e) { return null; } } public static Rgba parseRgbCssColor(String cssColor) { int r,g,b,a; String[] splitStrings = cssColor.trim().toLowerCase().split("[, \\(\\);]"); String[] values = new String[4]; ArrayUtils.filter(splitStrings, new Function<String,Boolean>(){ @Override public Boolean apply(String arg) { return Strings.isNullOrEmpty(arg); }}); if (values[0].equals("rgba")) { a = Integer.valueOf(values[4]); } else if (values[0].equals("rgb")) { a = 0; } else { // Can't parse this! // TODO: perhaps just return some default color? throw new RuntimeException("Can't parse css color string: " + cssColor); } r = Integer.valueOf(values[1]); g = Integer.valueOf(values[2]); b = Integer.valueOf(values[3]); return new Rgba(r,g,b,a); } }