/**
* Copyright (C) 2016 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.common.client.dom;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.stream.Stream;
import com.google.gwt.dom.client.Style;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* Provides utility methods for interacting with the DOM.
*
* @author Max Barkley <mbarkley@redhat.com>
*/
public abstract class DOMUtil {
private DOMUtil() {}
/**
* @param element
* Must not be null.
* @return If the given element has any child elements, return an optional containing the first child element.
* Otherwise return an empty optional.
*/
public static Optional<Element> getFirstChildElement(final Element element) {
for (final Node child : nodeIterable(element.getChildNodes())) {
if (isElement(child)) {
return Optional.ofNullable((Element) child);
}
}
return Optional.empty();
}
/**
* @param element
* Must not be null.
* @return If the given element has any child elements, return an optional containing the last child element.
* Otherwise return an empty optional.
*/
public static Optional<Element> getLastChildElement(final Element element) {
final NodeList children = element.getChildNodes();
for (int i = children.getLength()-1; i > -1; i--) {
if (isElement(children.item(i))) {
return Optional.ofNullable((Element) children.item(i));
}
}
return Optional.empty();
}
/**
* @param node
* Must not be null.
* @return True iff the given node is an element.
*/
public static boolean isElement(final Node node) {
return node.getNodeType() == Node.ELEMENT_NODE;
}
/**
* @param nodeList
* Must not be null.
* @return An iterable for the given node list.
*/
public static Iterable<Node> nodeIterable(final NodeList nodeList) {
return () -> DOMUtil.nodeIterator(nodeList);
}
/**
* @param nodeList
* Must not be null.
* @return An iterator for the given node list.
*/
public static Iterator<Node> nodeIterator(final NodeList nodeList) {
return new Iterator<Node>() {
int index = 0;
@Override
public boolean hasNext() {
return index < nodeList.getLength();
}
@Override
public Node next() {
if (hasNext()) {
return nodeList.item(index++);
}
else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* @param nodeList
* Must not be null.
* @return An iterable for the given node list that ignores non-element nodes.
*/
public static Iterable<Element> elementIterable(final NodeList nodeList) {
return () -> elementIterator(nodeList);
}
/**
* @param nodeList
* Must not be null.
* @return An iterator for the given node list that ignores non-element nodes.
*/
public static Iterator<Element> elementIterator(final NodeList nodeList) {
return new Iterator<Element>() {
int i = 0;
@Override
public boolean hasNext() {
while (i < nodeList.getLength() && !isElement(nodeList.item(i))) {
i++;
}
return i < nodeList.getLength();
}
@Override
public Element next() {
if (hasNext()) {
return (Element) nodeList.item(i++);
}
else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Detaches an element from its parent.
*
* @param element
* Must not be null.
* @return True if calling this method detaches the given element from a parent node. False if there is no parent to
* be removed from.
*/
public static boolean removeFromParent(final Element element) {
if (element.getParentElement() != null) {
element.getParentElement().removeChild(element);
return true;
}
else {
return false;
}
}
/**
* Detaches all children from a node.
*
* @param node
* Must not be null.
* @return True iff any children were detached by this call.
*/
public static boolean removeAllChildren(final Node node) {
final boolean hadChildren = node.getLastChild() != null;
while (node.getLastChild() != null) {
node.removeChild(node.getLastChild());
}
return hadChildren;
}
/**
* Detaches all element children from a node.
*
* @param node
* Must not be null.
* @return True iff any element children were detached by this call.
*/
public static boolean removeAllElementChildren(final Node node) {
boolean elementRemoved = false;
for (final Element child : elementIterable(node.getChildNodes())) {
node.removeChild(child);
elementRemoved = true;
}
return elementRemoved;
}
/**
* Removes a CSS class from an element's class list.
*
* @param element
* Must not be null.
* @param className
* The name of a CSS class. Must not be null.
* @return True if the given class was removed from the given element. False if the given element did not have the
* given class as part of its class list.
*/
public static boolean removeCSSClass(final HTMLElement element, final String className) {
if (hasCSSClass(element, className)) {
element.getClassList().remove(className);
return true;
}
else {
return false;
}
}
/**
* Adds a CSS class to an element's class list.
*
* @param element
* Must not be null.
* @param className
* The name of a CSS class. Must not be null.
* @return True if the given class was added to the given element. False if the given element already had the
* given class as part of its class list.
*/
public static boolean addCSSClass(final HTMLElement element, final String className) {
if (hasCSSClass(element, className)) {
return false;
}
else {
element.getClassList().add(className);
return true;
}
}
/**
* @param element
* Must not be null.
* @param className
* The name of a CSS class. Must not be null.
* @return True iff the given element has the given CSS class as part of its class list.
*/
public static boolean hasCSSClass(final HTMLElement element, final String className) {
return element.getClassList().contains(className);
}
/**
* @param tokenList
* Must not be null.
* @return A sequential, ordered {@link Stream} of tokens from the given {@link DOMTokenList}.
*/
public static Stream<String> tokenStream(final DOMTokenList tokenList) {
return Stream
.iterate(0, n -> n + 1)
.limit(tokenList.getLength())
.map(i -> tokenList.item(i));
}
/**
* @param styleDeclaration
* Must not be null.
* @return A stream of property names from the given style declaration.
*/
public static Stream<String> cssPropertyNameStream(final CSSStyleDeclaration styleDeclaration) {
return Arrays
.stream(styleDeclaration.getCssText() != null
? styleDeclaration.getCssText().split(";") : new String[0])
.map(style -> style.split(":", 2)[0].trim())
.filter(propertyName -> !propertyName.isEmpty());
}
/**
* Appends the underlying {@link HTMLElement} of a {@link Widget} to another {@link HTMLElement}, in a way that does
* not break GWT Widget events.
*
* @param parent
* The parent element that is appended to. Must not be null.
* @param child
* The child Widget, whose underlying HTML element will be appended to the parent. Must not be null.
*/
public static void appendWidgetToElement(final HTMLElement parent, final IsWidget child) {
appendWidgetToElement(parent, child.asWidget());
}
/**
* Appends the underlying {@link HTMLElement} of a {@link Widget} to another {@link HTMLElement}, in a way that does
* not break GWT Widget events.
*
* @param parent
* The parent element that is appended to. Must not be null.
* @param child
* The child Widget, whose underlying HTML element will be appended to the parent. Must not be null.
*/
public static void appendWidgetToElement(final HTMLElement parent, final Widget child) {
if (child.isAttached()) {
child.removeFromParent();
}
onAttach(child);
RootPanel.detachOnWindowClose(child);
parent.appendChild(nativeCast(child.getElement()));
}
/**
* <p>
* Remove a {@link Widget} from its parent. Use this to undo calls to
* {@link #appendWidgetToElement(HTMLElement, IsWidget)}.
*
* <p>
* This works like calling {@code child.asWidget().removeFromParent()} except that if {@code child} is in the
* {@link RootPanel#isInDetachList(Widget) dettach list} its underlying HTML element will also be removed from its
* parent.
*
* @param child
* The child Widget, whose underlying HTML element will be removed from the parent. Must not be null.
*/
public static void removeFromParent(final IsWidget child) {
removeFromParent(child.asWidget());
}
/**
* <p>
* Remove a {@link Widget} from its parent. Use this to undo calls to
* {@link #appendWidgetToElement(HTMLElement, Widget)}.
*
* <p>
* This works like calling {@code child.removeFromParent()} except that if {@code child} is in the
* {@link RootPanel#isInDetachList(Widget) dettach list} its underlying HTML element will also be removed from its
* parent.
*
* @param child
* The child Widget, whose underlying HTML element will be removed from the parent. Must not be null.
*/
public static void removeFromParent(final Widget child) {
final boolean wasInDettachList = RootPanel.isInDetachList(child);
child.removeFromParent();
if (wasInDettachList) {
child.getElement().removeFromParent();
}
}
/**
* Adds a unique enumerated CSS class to an element's class list removing all others
* in the enumerated {@link Style.HasCssName}. Other CSS classes not in the enumerated
* {@link Style.HasCssName} are preserved.
*
* @param element
* Must not be null.
* @param enumClass
* The {@link Style.HasCssName} class. Must not be null.
* @param style
* The enumerated {@link Style.HasCssName} element. Must not be null.
*/
public static <E extends Style.HasCssName, F extends Enum<? extends Style.HasCssName>> void addUniqueEnumStyleName(final HTMLElement element,
final Class<F> enumClass,
final E style) {
removeEnumStyleNames(element,
enumClass);
addEnumStyleName(element,
style);
}
/**
* Removes all of the enumerated CSS classes on an element's class list.
*
* @param element
* Must not be null.
* @param enumClass
* The {@link Style.HasCssName} class. Must not be null.
*/
public static <E extends Enum<? extends Style.HasCssName>> void removeEnumStyleNames(final HTMLElement element,
final Class<E> enumClass) {
for (final Enum<? extends Style.HasCssName> constant : enumClass.getEnumConstants()) {
final String cssClass = ((Style.HasCssName) constant).getCssName();
if (cssClass != null && !cssClass.isEmpty()) {
removeCSSClass(element,
cssClass);
}
}
}
/**
* Adds an enumerated CSS class to an element's class list.
*
* @param element
* Must not be null.
* @param style
* The enumerated {@link Style.HasCssName} element. Must not be null.
*/
public static <E extends Style.HasCssName> void addEnumStyleName(final HTMLElement element,
final E style) {
if (style != null && style.getCssName() != null && !style.getCssName().isEmpty()) {
addCSSClass(element,
style.getCssName());
}
}
/**
* Removes the enumerated CSS class from an element's class list.
*
* @param element
* Must not be null.
* @param style
* The enumerated {@link Style.HasCssName} element. Must not be null.
*/
public static <E extends Style.HasCssName> void removeEnumStyleName(final HTMLElement element,
final E style) {
if (style != null && style.getCssName() != null && !style.getCssName().isEmpty()) {
removeCSSClass(element,
style.getCssName());
}
}
private static native void onAttach(Widget w)/*-{
w.@com.google.gwt.user.client.ui.Widget::onAttach()();
}-*/;
private static native HTMLElement nativeCast(com.google.gwt.dom.client.Element gwtElement)/*-{
return gwtElement;
}-*/;
}