// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.client.util.dom;
import com.google.collide.client.util.BrowserUtils;
import com.google.collide.client.util.Elements;
import com.google.gwt.user.client.DOM;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.MouseEvent;
import elemental.html.ClientRect;
import elemental.html.DivElement;
import elemental.html.Element;
/**
* Utility methods for DOM manipulation.
*
*/
public final class DomUtils {
public static class Offset {
public int top = 0;
public int left = 0;
private Offset() {
}
private Offset(int top, int left) {
this.top = top;
this.left = left;
}
}
private static final EventListener STOP_PROPAGATION_EVENT_LISTENER = new EventListener() {
@Override
public void handleEvent(Event evt) {
evt.stopPropagation();
evt.preventDefault();
}
};
/**
* Returns the client offset to the top-left of the given element.
*/
@Deprecated
public static Offset calculateElementClientOffset(Element element) {
return calculateElementOffset(element, null, false);
}
/**
* Returns an offset to the top-left of a child element relative to the
* top-left of an ancestor element, optionally including any scroll top or
* left in elements from the ancestor (inclusive) to the child (exclusive).
*
* @param ancestorElement optional, if null the offset from the top-left of
* the page will be given. Should not be the childElement.
*/
@Deprecated
public static Offset calculateElementOffset(
Element childElement, Element ancestorElement, boolean includeScroll) {
Offset offset = new Offset();
Element element = childElement;
for (; element.getOffsetParent() != null && element != ancestorElement; element =
element.getOffsetParent()) {
offset.top += element.getOffsetTop();
offset.left += element.getOffsetLeft();
if (!includeScroll) {
offset.top -= element.getOffsetParent().getScrollTop();
offset.left -= element.getOffsetParent().getScrollLeft();
}
}
return offset;
}
/**
* Wrapper for getting the offsetX from a mouse event that provides a fallback
* implementation for Firefox. (See
* https://bugzilla.mozilla.org/show_bug.cgi?id=122665#c3 )
*/
public static int getOffsetX(MouseEvent event) {
if (BrowserUtils.isFirefox()) {
return event.getClientX()
- calculateElementClientOffset((Element) event.getTarget()).left;
} else {
return event.getOffsetX();
}
}
/**
* @see #getOffsetX(MouseEvent)
*/
public static int getOffsetY(MouseEvent event) {
if (BrowserUtils.isFirefox()) {
return event.getClientY()
- calculateElementClientOffset((Element) event.getTarget()).top;
} else {
return event.getOffsetY();
}
}
public static Element getNthChild(Element element, int index) {
Element child = element.getFirstChildElement();
while (child != null && index > 0) {
--index;
child = child.getNextSiblingElement();
}
return child;
}
public static Element getNthChildWithClassName(Element element, int index, String className) {
Element child = element.getFirstChildElement();
while (child != null) {
if (child.hasClassName(className)) {
--index;
if (index < 0) {
break;
}
}
child = child.getNextSiblingElement();
}
return child;
}
/**
* @return number of previous sibling elements that have the given class
*/
public static int getSiblingIndexWithClassName(Element element, String className) {
int index = 0;
while (element != null) {
element = (Element) element.getPreviousSibling();
if (element != null && element.hasClassName(className)) {
++index;
}
}
return index;
}
public static Element getFirstElementByClassName(Element element, String className) {
return (Element) element.getElementsByClassName(className).item(0);
}
public static DivElement appendDivWithTextContent(Element root, String className, String text) {
DivElement element = Elements.createDivElement(className);
element.setTextContent(text);
root.appendChild(element);
return element;
}
/**
* Ensures that the {@code scrollable} element is scrolled such that
* {@code target} is visible.
*
* Note: This can trigger a synchronous layout.
*/
public static boolean ensureScrolledTo(Element scrollable, Element target) {
ClientRect targetBounds = target.getBoundingClientRect();
ClientRect scrollableBounds = scrollable.getBoundingClientRect();
int deltaBottoms = (int) (targetBounds.getBottom() - scrollableBounds.getBottom());
int deltaTops = (int) (targetBounds.getTop() - scrollableBounds.getTop());
if (deltaTops >= 0 && deltaBottoms <= 0) {
// In bounds
return false;
}
if (targetBounds.getHeight() > scrollableBounds.getHeight() || deltaTops < 0) {
/*
* Selected is taller than viewport height or selected is scrolled above
* viewport, so set to top
*/
scrollable.setScrollTop(scrollable.getScrollTop() + deltaTops);
} else {
// Selected is scrolled below viewport
scrollable.setScrollTop(scrollable.getScrollTop() + deltaBottoms);
}
return true;
}
/**
* Checks whether the given {@code target} element is fully visible in
* {@code scrollable}'s scrolled viewport.
*
* Note: This can trigger a synchronous layout.
*/
public static boolean isFullyInScrollViewport(Element scrollable, Element target) {
ClientRect targetBounds = target.getBoundingClientRect();
ClientRect scrollableBounds = scrollable.getBoundingClientRect();
return targetBounds.getTop() >= scrollableBounds.getTop()
&& targetBounds.getBottom() <= scrollableBounds.getBottom();
}
/**
* Stops propagation for the common mouse events (down, move, up, click,
* dblclick).
*/
public static void stopMousePropagation(Element element) {
element.addEventListener(Event.MOUSEDOWN, STOP_PROPAGATION_EVENT_LISTENER, false);
element.addEventListener(Event.MOUSEMOVE, STOP_PROPAGATION_EVENT_LISTENER, false);
element.addEventListener(Event.MOUSEUP, STOP_PROPAGATION_EVENT_LISTENER, false);
element.addEventListener(Event.CLICK, STOP_PROPAGATION_EVENT_LISTENER, false);
element.addEventListener(Event.DBLCLICK, STOP_PROPAGATION_EVENT_LISTENER, false);
}
/**
* Prevent propagation of scrolling to parent containers on mouse wheeling,
* when target container can not be scrolled anymore.
*/
public static void preventExcessiveScrollingPropagation(final Element container) {
container.addEventListener(Event.MOUSEWHEEL, new EventListener() {
@Override
public void handleEvent(Event evt) {
int deltaY = DOM.eventGetMouseWheelVelocityY((com.google.gwt.user.client.Event) evt);
int scrollTop = container.getScrollTop();
if (deltaY < 0) {
if (scrollTop == 0) {
evt.preventDefault();
}
} else {
int scrollBottom = scrollTop + (int) container.getBoundingClientRect().getHeight();
if (scrollBottom == container.getScrollHeight()) {
evt.preventDefault();
}
}
evt.stopPropagation();
}
}, false);
}
/**
* Doing Elements.asJsElement(button).setDisabled(true); doesn't work for buttons, possibly
* because they're actually AnchorElements
*/
public static void setDisabled(Element element, boolean disabled) {
if (disabled) {
element.setAttribute("disabled", "disabled");
} else {
element.removeAttribute("disabled");
}
}
public static boolean getDisabled(Element element) {
return element.hasAttribute("disabled");
}
/**
* @return true if the provided element or one of its children have focus.
*/
public static boolean isElementOrChildFocused(Element element) {
Element active = element.getOwnerDocument().getActiveElement();
return element.contains(active);
}
private DomUtils() {} // COV_NF_LINE
}