gwt.material.design.addins.client.cutout.MaterialCutOut Maven / Gradle / Ivy
/*
* #%L
* GwtMaterial
* %%
* Copyright (C) 2015 - 2017 GwtMaterialDesign
* %%
* 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.
* #L%
*/
package gwt.material.design.addins.client.cutout;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.logical.shared.*;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import gwt.material.design.addins.client.base.constants.AddinsCssName;
import gwt.material.design.client.base.HasCircle;
import gwt.material.design.client.base.HasDurationTransition;
import gwt.material.design.client.base.MaterialWidget;
import gwt.material.design.client.base.helper.ColorHelper;
import gwt.material.design.client.constants.Color;
import static gwt.material.design.jquery.client.api.JQuery.$;
//@formatter:off
/**
* MaterialCutOut is a fullscreen dialog-like component to show users about new
* features or important elements of the document.
*
* You can use {@link CloseHandler}s to be notified when the cut out is closed.
*
*
XML Namespace Declaration
*
* {@code
* xmlns:ma='urn:import:gwt.material.design.addins.client'
* }
*
*
*
UiBinder Usage:
*
*
* {@code
*
*
*
* }
*
*
*
Java Usage:
* {@code
* MaterialCutOut cutOut = ... //create using new or using UiBinder
* cutOut.setTarget(myTargetWidget); //the widget or element you want to focus
* cutOut.open(); //shows the dialog over the page
* }
*
*
Custom styling:
You use change the cut out style by using the
* material-cutout
class, and material-cutout-focus
* class for the focusElement box.
*
*
Notice:
On some iOS devices, on mobile Safari, the CutOut may not open when the
* {@link #setCircle(boolean)} is set to true
. This is because of problems on Safari
* with box-shadows over rounded borders. To avoid this issue you can disable the circle. Check the
* issue 227 for details.
*
* @author gilberto-torrezan
* @see Material Cutouts
*/
// @formatter:on
public class MaterialCutOut extends MaterialWidget implements HasCloseHandlers,
HasOpenHandlers, HasCircle, HasDurationTransition {
private Color backgroundColor = Color.BLUE;
private int cutOutPadding = 10;
private double opacity = 0.8;
private boolean animated = true;
private String animationTimingFunction = "ease";
private String backgroundSize;
private String computedBackgroundColor;
private boolean circle = false;
private boolean autoAddedToDocument = false;
private String viewportOverflow;
private Element targetElement;
private Element focusElement;
private int duration = 500;
public MaterialCutOut() {
super(Document.get().createDivElement(), AddinsCssName.MATERIAL_CUTOUT);
focusElement = Document.get().createDivElement();
getElement().appendChild(focusElement);
getElement().getStyle().setOverflow(Overflow.HIDDEN);
getElement().getStyle().setDisplay(Display.NONE);
}
public MaterialCutOut(Color backgroundColor, Boolean circle, Double opacity) {
this();
setBackgroundColor(backgroundColor);
setCircle(circle);
setOpacity(opacity);
}
/**
* Opens the dialog cut out taking all the screen. The target element should
* be set before calling this method.
*
* @throws IllegalStateException if the target element is null
* @see #setTarget(Widget)
*/
public void open() {
setCutOutStyle();
if (targetElement == null) {
throw new IllegalStateException("The target element should be set before calling open().");
}
targetElement.scrollIntoView();
if (computedBackgroundColor == null) {
setupComputedBackgroundColor();
}
//temporarily disables scrolling by setting the overflow of the page to hidden
Style docStyle = Document.get().getDocumentElement().getStyle();
viewportOverflow = docStyle.getOverflow();
docStyle.setProperty("overflow", "hidden");
if (backgroundSize == null) {
backgroundSize = body().width() + 300 + "px";
}
setupTransition();
if (animated) {
focusElement.getStyle().setProperty("boxShadow", "0px 0px 0px 0rem " + computedBackgroundColor);
//the animation will take place after the boxshadow is set by the deferred command
Scheduler.get().scheduleDeferred(() -> {
focusElement.getStyle().setProperty("boxShadow", "0px 0px 0px " + backgroundSize + " " + computedBackgroundColor);
});
} else {
focusElement.getStyle().setProperty("boxShadow", "0px 0px 0px " + backgroundSize + " " + computedBackgroundColor);
}
if (circle) {
focusElement.getStyle().setProperty("WebkitBorderRadius", "50%");
focusElement.getStyle().setProperty("borderRadius", "50%");
// Temporary fixed for the IOS Issue on recalculation of the border radius
focusElement.getStyle().setProperty("webkitBorderTopLeftRadius", "49.9%");
focusElement.getStyle().setProperty("borderTopLeftRadius", "49.9%");
} else {
focusElement.getStyle().clearProperty("WebkitBorderRadius");
focusElement.getStyle().clearProperty("borderRadius");
focusElement.getStyle().clearProperty("webkitBorderTopLeftRadius");
focusElement.getStyle().clearProperty("borderTopLeftRadius");
}
setupCutOutPosition(focusElement, targetElement, cutOutPadding, circle);
setupWindowHandlers();
getElement().getStyle().clearDisplay();
// verify if the component is added to the document (via UiBinder for
// instance)
if (getParent() == null) {
autoAddedToDocument = true;
RootPanel.get().add(this);
}
OpenEvent.fire(this, this);
}
protected void setCutOutStyle() {
Style style = getElement().getStyle();
style.setWidth(100, Unit.PCT);
style.setHeight(100, Unit.PCT);
style.setPosition(Position.FIXED);
style.setTop(0, Unit.PX);
style.setLeft(0, Unit.PX);
style.setZIndex(10000);
focusElement.setClassName(AddinsCssName.MATERIAL_CUTOUT_FOCUS);
style = focusElement.getStyle();
style.setProperty("content", "\'\'");
style.setPosition(Position.ABSOLUTE);
style.setZIndex(-1);
}
/**
* Closes the cut out. It is the same as calling
* {@link #close(boolean)} with false
.
*/
public void close() {
this.close(false);
}
/**
* Closes the cut out.
*
* @param autoClosed Notifies with the dialog was auto closed or closed by user action
*/
public void close(boolean autoClosed) {
//restore the old overflow of the page
Document.get().getDocumentElement().getStyle().setProperty("overflow", viewportOverflow);
getElement().getStyle().setDisplay(Display.NONE);
getHandlerRegistry().clearHandlers();
// if the component added himself to the document, it must remove
// himself too
if (autoAddedToDocument) {
this.removeFromParent();
autoAddedToDocument = false;
}
CloseEvent.fire(this, this, autoClosed);
}
@Override
public void setBackgroundColor(Color bgColor) {
backgroundColor = bgColor;
//resetting the computedBackgroundColor
computedBackgroundColor = null;
}
@Override
public Color getBackgroundColor() {
return backgroundColor;
}
@Override
public void setOpacity(double opacity) {
this.opacity = opacity;
//resetting the computedBackgroundColor
computedBackgroundColor = null;
}
@Override
public Double getOpacity() {
return opacity;
}
/**
* @return the animation timing fucntion of the opening cut out
*/
public String getAnimationTimingFunction() {
return animationTimingFunction;
}
/**
* Sets the animation timing fucntion of the opening cut out.
*
* @param animationTimingFunction The speed curve of the animation, such as ease (the default), linear and
* ease-in-out
*/
public void setAnimationTimingFunction(String animationTimingFunction) {
this.animationTimingFunction = animationTimingFunction;
}
/**
* Sets if the cut out should be rendered as a circle or a simple rectangle.
* Circle is better for targets with same width and height. The default is
* false
(is a rectangle).
*/
@Override
public void setCircle(boolean circle) {
this.circle = circle;
}
/**
* @return The if the cut out should be rendered as a circle or a simple
* rectangle
*/
@Override
public boolean isCircle() {
return circle;
}
/**
* Sets the padding in pixels of the cut out focusElement in relation to the target
* element. The default is 10.
*/
public void setCutOutPadding(int cutOutPadding) {
this.cutOutPadding = cutOutPadding;
}
/**
* @return The padding in pixels of the cut out focusElement in relation to the
* target element
*/
public int getCutOutPadding() {
return cutOutPadding;
}
/**
* Sets the target element to be focused by the cut out.
*/
public void setTarget(Element targetElement) {
this.targetElement = targetElement;
}
/**
* Sets the target widget to be focused by the cut out. Its the same as
* calling setTarget(widget.getElement());
*
* @see #setTarget(Element)
*/
public void setTarget(Widget widget) {
setTarget(widget.getElement());
}
/**
* @return The target element to be focused
*/
public Element getTargetElement() {
return targetElement;
}
/**
* Enables or disables the open animation of the cut out.
* The default is true
.
*/
public void setAnimated(boolean animated) {
this.animated = animated;
}
/**
* @return If the animation of the cut out is enabled when opening.
*/
public boolean isAnimated() {
return animated;
}
/**
* Sets the radius size of the Cut Out background. By default, it takes the whole page
* by using 100rem as size.
*
* @param backgroundSize The size of the background of the Cut Out. You can use any supported
* CSS unit for box shadows, such as rem and px.
*/
public void setBackgroundSize(String backgroundSize) {
this.backgroundSize = backgroundSize;
}
/**
* @return The radius size of the background of the Cut Out.
*/
public String getBackgroundSize() {
return backgroundSize;
}
/**
* Setups the cut out position when the screen changes size or is scrolled.
*/
protected void setupCutOutPosition(Element cutOut, Element relativeTo, int padding, boolean circle) {
float top = relativeTo.getOffsetTop() - (Math.max($("html").scrollTop(), $("body").scrollTop()));
float left = relativeTo.getAbsoluteLeft();
float width = relativeTo.getOffsetWidth();
float height = relativeTo.getOffsetHeight();
if (circle) {
if (width != height) {
float dif = width - height;
if (width > height) {
height += dif;
top -= dif / 2;
} else {
dif = -dif;
width += dif;
left -= dif / 2;
}
}
}
top -= padding;
left -= padding;
width += padding * 2;
height += padding * 2;
$(cutOut).css("top", top + "px");
$(cutOut).css("left", left + "px");
$(cutOut).css("width", width + "px");
$(cutOut).css("height", height + "px");
}
/**
* Configures a resize handler and a scroll handler on the window to
* properly adjust the Cut Out.
*/
protected void setupWindowHandlers() {
registerHandler(Window.addResizeHandler(event -> setupCutOutPosition(focusElement, targetElement, cutOutPadding, circle)));
registerHandler(Window.addWindowScrollHandler(event -> setupCutOutPosition(focusElement, targetElement, cutOutPadding, circle)));
}
protected void setupTransition() {
if (animated) {
focusElement.getStyle().setProperty("WebkitTransition", "box-shadow " + getDuration() + "ms " + animationTimingFunction);
focusElement.getStyle().setProperty("transition", "box-shadow " + getDuration() + "ms " + animationTimingFunction);
} else {
focusElement.getStyle().clearProperty("WebkitTransition");
focusElement.getStyle().clearProperty("transition");
}
}
/**
* Gets the computed background color, based on the backgroundColor CSS
* class.
*/
protected void setupComputedBackgroundColor() {
// temp is just a widget created to evaluate the computed background
// color
MaterialWidget temp = new MaterialWidget(Document.get().createDivElement());
temp.setBackgroundColor(backgroundColor);
// setting a style to make it invisible for the user
Style style = temp.getElement().getStyle();
style.setPosition(Position.FIXED);
style.setWidth(1, Unit.PX);
style.setHeight(1, Unit.PX);
style.setLeft(-10, Unit.PX);
style.setTop(-10, Unit.PX);
style.setZIndex(-10000);
// adding it to the body (on Chrome the component must be added to the
// DOM before getting computed values).
String computed = ColorHelper.setupComputedBackgroundColor(backgroundColor);
// convert rgb to rgba, considering the opacity field
if (opacity < 1 && computed.startsWith("rgb(")) {
computed = computed.replace("rgb(", "rgba(").replace(")", ", " + opacity + ")");
}
computedBackgroundColor = computed;
}
public Element getFocusElement() {
return focusElement;
}
@Override
public void setDuration(int duration) {
this.duration = duration;
}
@Override
public int getDuration() {
return duration;
}
@Override
public HandlerRegistration addCloseHandler(final CloseHandler handler) {
return addHandler(handler, CloseEvent.getType());
}
@Override
public HandlerRegistration addOpenHandler(OpenHandler handler) {
return addHandler(handler, OpenEvent.getType());
}
}