001/* 002 * #%L 003 * GwtMaterial 004 * %% 005 * Copyright (C) 2015 - 2017 GwtMaterialDesign 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package gwt.material.design.addins.client.cutout; 021 022import com.google.gwt.core.client.Scheduler; 023import com.google.gwt.dom.client.Document; 024import com.google.gwt.dom.client.Element; 025import com.google.gwt.dom.client.Style; 026import com.google.gwt.dom.client.Style.Display; 027import com.google.gwt.dom.client.Style.Overflow; 028import com.google.gwt.dom.client.Style.Position; 029import com.google.gwt.dom.client.Style.Unit; 030import com.google.gwt.event.logical.shared.*; 031import com.google.gwt.event.shared.HandlerRegistration; 032import com.google.gwt.user.client.Window; 033import com.google.gwt.user.client.ui.RootPanel; 034import com.google.gwt.user.client.ui.Widget; 035import gwt.material.design.addins.client.base.constants.AddinsCssName; 036import gwt.material.design.client.base.HasCircle; 037import gwt.material.design.client.base.HasDurationTransition; 038import gwt.material.design.client.base.MaterialWidget; 039import gwt.material.design.client.base.helper.ColorHelper; 040import gwt.material.design.client.constants.Color; 041 042import static gwt.material.design.jquery.client.api.JQuery.$; 043 044//@formatter:off 045 046/** 047 * MaterialCutOut is a fullscreen modal-like component to show users about new 048 * features or important elements of the document. 049 * <p> 050 * You can use {@link CloseHandler}s to be notified when the cut out is closed. 051 * <p> 052 * <h3>XML Namespace Declaration</h3> 053 * <pre> 054 * {@code 055 * xmlns:ma='urn:import:gwt.material.design.addins.client' 056 * } 057 * </pre> 058 * <p> 059 * <h3>UiBinder Usage:</h3> 060 * <p> 061 * <pre> 062 * {@code 063 * <ma:cutout.MaterialCutOut ui:field="cutOut"> 064 * <!-- add any widgets here --> 065 * </ma:cutout.MaterialCutOut> 066 * } 067 * </pre> 068 * <p> 069 * <h3>Java Usage:</h3> 070 * {@code 071 * MaterialCutOut cutOut = ... //create using new or using UiBinder 072 * cutOut.setTarget(myTargetWidget); //the widget or element you want to focus 073 * cutOut.open(); //shows the modal over the page 074 * } 075 * <p> 076 * <h3>Custom styling:</h3> You use change the cut out style by using the 077 * <code>material-cutout</code> class, and <code>material-cutout-focus</code> 078 * class for the focusElement box. 079 * <p> 080 * <h3>Notice:</h3>On some iOS devices, on mobile Safari, the CutOut may not open when the 081 * {@link #setCircle(boolean)} is set to <code>true</code>. This is because of problems on Safari 082 * with box-shadows over rounded borders. To avoid this issue you can disable the circle. Check the 083 * <a href="https://github.com/GwtMaterialDesign/gwt-material/issues/227">issue 227</a> for details. 084 * 085 * @author gilberto-torrezan 086 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#cutouts">Material Cutouts</a> 087 */ 088// @formatter:on 089public class MaterialCutOut extends MaterialWidget implements HasCloseHandlers<MaterialCutOut>, 090 HasOpenHandlers<MaterialCutOut>, HasCircle, HasDurationTransition { 091 092 private Color backgroundColor = Color.BLUE; 093 private int cutOutPadding = 10; 094 private double opacity = 0.8; 095 private boolean animated = true; 096 private String animationTimingFunction = "ease"; 097 private String backgroundSize; 098 private String computedBackgroundColor; 099 private boolean circle = false; 100 private boolean autoAddedToDocument = false; 101 private String viewportOverflow; 102 private Element targetElement; 103 private Element focusElement; 104 private int duration = 500; 105 106 public MaterialCutOut() { 107 super(Document.get().createDivElement(), AddinsCssName.MATERIAL_CUTOUT); 108 109 focusElement = Document.get().createDivElement(); 110 getElement().appendChild(focusElement); 111 112 getElement().getStyle().setOverflow(Overflow.HIDDEN); 113 getElement().getStyle().setDisplay(Display.NONE); 114 } 115 116 public MaterialCutOut(Color backgroundColor, Boolean circle, Double opacity) { 117 this(); 118 setBackgroundColor(backgroundColor); 119 setCircle(circle); 120 setOpacity(opacity); 121 } 122 123 /** 124 * Opens the modal cut out taking all the screen. The target element should 125 * be set before calling this method. 126 * 127 * @throws IllegalStateException if the target element is <code>null</code> 128 * @see #setTarget(Widget) 129 */ 130 public void open() { 131 setCutOutStyle(); 132 133 if (targetElement == null) { 134 throw new IllegalStateException("The target element should be set before calling open()."); 135 } 136 targetElement.scrollIntoView(); 137 138 if (computedBackgroundColor == null) { 139 setupComputedBackgroundColor(); 140 } 141 142 //temporarily disables scrolling by setting the overflow of the page to hidden 143 Style docStyle = Document.get().getDocumentElement().getStyle(); 144 viewportOverflow = docStyle.getOverflow(); 145 docStyle.setProperty("overflow", "hidden"); 146 147 if (backgroundSize == null) { 148 backgroundSize = body().width() + 300 + "px"; 149 } 150 151 setupTransition(); 152 if (animated) { 153 focusElement.getStyle().setProperty("boxShadow", "0px 0px 0px 0rem " + computedBackgroundColor); 154 155 //the animation will take place after the boxshadow is set by the deferred command 156 Scheduler.get().scheduleDeferred(() -> { 157 focusElement.getStyle().setProperty("boxShadow", "0px 0px 0px " + backgroundSize + " " + computedBackgroundColor); 158 }); 159 } else { 160 focusElement.getStyle().setProperty("boxShadow", "0px 0px 0px " + backgroundSize + " " + computedBackgroundColor); 161 } 162 163 if (circle) { 164 focusElement.getStyle().setProperty("WebkitBorderRadius", "50%"); 165 focusElement.getStyle().setProperty("borderRadius", "50%"); 166 } else { 167 focusElement.getStyle().clearProperty("WebkitBorderRadius"); 168 focusElement.getStyle().clearProperty("borderRadius"); 169 } 170 setupCutOutPosition(focusElement, targetElement, cutOutPadding, circle); 171 172 setupWindowHandlers(); 173 getElement().getStyle().clearDisplay(); 174 175 // verify if the component is added to the document (via UiBinder for 176 // instance) 177 if (getParent() == null) { 178 autoAddedToDocument = true; 179 RootPanel.get().add(this); 180 } 181 OpenEvent.fire(this, this); 182 } 183 184 protected void setCutOutStyle() { 185 Style style = getElement().getStyle(); 186 style.setWidth(100, Unit.PCT); 187 style.setHeight(100, Unit.PCT); 188 style.setPosition(Position.FIXED); 189 style.setTop(0, Unit.PX); 190 style.setLeft(0, Unit.PX); 191 style.setZIndex(10000); 192 193 focusElement.setClassName(AddinsCssName.MATERIAL_CUTOUT_FOCUS); 194 style = focusElement.getStyle(); 195 style.setProperty("content", "\'\'"); 196 style.setPosition(Position.ABSOLUTE); 197 style.setZIndex(-1); 198 } 199 200 /** 201 * Closes the cut out. It is the same as calling 202 * {@link #close(boolean)} with <code>false</code>. 203 */ 204 public void close() { 205 this.close(false); 206 } 207 208 /** 209 * Closes the cut out. 210 * 211 * @param autoClosed Notifies with the modal was auto closed or closed by user action 212 */ 213 public void close(boolean autoClosed) { 214 //restore the old overflow of the page 215 Document.get().getDocumentElement().getStyle().setProperty("overflow", viewportOverflow); 216 217 getElement().getStyle().setDisplay(Display.NONE); 218 219 getHandlerRegistry().clearHandlers(); 220 221 // if the component added himself to the document, it must remove 222 // himself too 223 if (autoAddedToDocument) { 224 this.removeFromParent(); 225 autoAddedToDocument = false; 226 } 227 CloseEvent.fire(this, this, autoClosed); 228 } 229 230 @Override 231 public void setBackgroundColor(Color bgColor) { 232 backgroundColor = bgColor; 233 //resetting the computedBackgroundColor 234 computedBackgroundColor = null; 235 } 236 237 @Override 238 public Color getBackgroundColor() { 239 return backgroundColor; 240 } 241 242 @Override 243 public void setOpacity(double opacity) { 244 this.opacity = opacity; 245 //resetting the computedBackgroundColor 246 computedBackgroundColor = null; 247 } 248 249 @Override 250 public double getOpacity() { 251 return opacity; 252 } 253 254 /** 255 * @return the animation timing fucntion of the opening cut out 256 */ 257 public String getAnimationTimingFunction() { 258 return animationTimingFunction; 259 } 260 261 /** 262 * Sets the animation timing fucntion of the opening cut out. 263 * 264 * @param animationTimingFunction The speed curve of the animation, such as ease (the default), linear and 265 * ease-in-out 266 */ 267 public void setAnimationTimingFunction(String animationTimingFunction) { 268 this.animationTimingFunction = animationTimingFunction; 269 } 270 271 /** 272 * Sets if the cut out should be rendered as a circle or a simple rectangle. 273 * Circle is better for targets with same width and height. The default is 274 * <code>false</code> (is a rectangle). 275 */ 276 @Override 277 public void setCircle(boolean circle) { 278 this.circle = circle; 279 } 280 281 /** 282 * @return The if the cut out should be rendered as a circle or a simple 283 * rectangle 284 */ 285 @Override 286 public boolean isCircle() { 287 return circle; 288 } 289 290 /** 291 * Sets the padding in pixels of the cut out focusElement in relation to the target 292 * element. The default is 10. 293 */ 294 public void setCutOutPadding(int cutOutPadding) { 295 this.cutOutPadding = cutOutPadding; 296 } 297 298 /** 299 * @return The padding in pixels of the cut out focusElement in relation to the 300 * target element 301 */ 302 public int getCutOutPadding() { 303 return cutOutPadding; 304 } 305 306 /** 307 * Sets the target element to be focused by the cut out. 308 */ 309 public void setTarget(Element targetElement) { 310 this.targetElement = targetElement; 311 } 312 313 /** 314 * Sets the target widget to be focused by the cut out. Its the same as 315 * calling setTarget(widget.getElement()); 316 * 317 * @see #setTarget(Element) 318 */ 319 public void setTarget(Widget widget) { 320 setTarget(widget.getElement()); 321 } 322 323 /** 324 * @return The target element to be focused 325 */ 326 public Element getTargetElement() { 327 return targetElement; 328 } 329 330 /** 331 * Enables or disables the open animation of the cut out. 332 * The default is <code>true</code>. 333 */ 334 public void setAnimated(boolean animated) { 335 this.animated = animated; 336 } 337 338 /** 339 * @return If the animation of the cut out is enabled when opening. 340 */ 341 public boolean isAnimated() { 342 return animated; 343 } 344 345 /** 346 * Sets the radius size of the Cut Out background. By default, it takes the whole page 347 * by using 100rem as size. 348 * 349 * @param backgroundSize The size of the background of the Cut Out. You can use any supported 350 * CSS unit for box shadows, such as rem and px. 351 */ 352 public void setBackgroundSize(String backgroundSize) { 353 this.backgroundSize = backgroundSize; 354 } 355 356 /** 357 * @return The radius size of the background of the Cut Out. 358 */ 359 public String getBackgroundSize() { 360 return backgroundSize; 361 } 362 363 /** 364 * Setups the cut out position when the screen changes size or is scrolled. 365 */ 366 protected void setupCutOutPosition(Element cutOut, Element relativeTo, int padding, boolean circle) { 367 float top = relativeTo.getAbsoluteTop() - body().scrollTop(); 368 float left = relativeTo.getAbsoluteLeft(); 369 370 float width = relativeTo.getOffsetWidth(); 371 float height = relativeTo.getOffsetHeight(); 372 373 if (circle) { 374 if (width != height) { 375 float dif = width - height; 376 if (width > height) { 377 height += dif; 378 top -= dif / 2; 379 } else { 380 dif = -dif; 381 width += dif; 382 left -= dif / 2; 383 } 384 } 385 } 386 387 top -= padding; 388 left -= padding; 389 width += padding * 2; 390 height += padding * 2; 391 392 $(cutOut).css("top", top + "px"); 393 $(cutOut).css("left", left + "px"); 394 $(cutOut).css("width", width + "px"); 395 $(cutOut).css("height", height + "px"); 396 } 397 398 /** 399 * Configures a resize handler and a scroll handler on the window to 400 * properly adjust the Cut Out. 401 */ 402 protected void setupWindowHandlers() { 403 404 registerHandler(Window.addResizeHandler(event -> setupCutOutPosition(focusElement, targetElement, cutOutPadding, circle))); 405 registerHandler(Window.addWindowScrollHandler(event -> setupCutOutPosition(focusElement, targetElement, cutOutPadding, circle))); 406 } 407 408 protected void setupTransition() { 409 if (animated) { 410 focusElement.getStyle().setProperty("WebkitTransition", "box-shadow " + getDuration() + "ms " + animationTimingFunction); 411 focusElement.getStyle().setProperty("transition", "box-shadow " + getDuration() + "ms " + animationTimingFunction); 412 } else { 413 focusElement.getStyle().clearProperty("WebkitTransition"); 414 focusElement.getStyle().clearProperty("transition"); 415 } 416 } 417 418 /** 419 * Gets the computed background color, based on the backgroundColor CSS 420 * class. 421 */ 422 protected void setupComputedBackgroundColor() { 423 // temp is just a widget created to evaluate the computed background 424 // color 425 MaterialWidget temp = new MaterialWidget(Document.get().createDivElement()); 426 temp.setBackgroundColor(backgroundColor); 427 428 // setting a style to make it invisible for the user 429 Style style = temp.getElement().getStyle(); 430 style.setPosition(Position.FIXED); 431 style.setWidth(1, Unit.PX); 432 style.setHeight(1, Unit.PX); 433 style.setLeft(-10, Unit.PX); 434 style.setTop(-10, Unit.PX); 435 style.setZIndex(-10000); 436 437 // adding it to the body (on Chrome the component must be added to the 438 // DOM before getting computed values). 439 String computed = ColorHelper.setupComputedBackgroundColor(backgroundColor); 440 441 // convert rgb to rgba, considering the opacity field 442 if (opacity < 1 && computed.startsWith("rgb(")) { 443 computed = computed.replace("rgb(", "rgba(").replace(")", ", " + opacity + ")"); 444 } 445 446 computedBackgroundColor = computed; 447 } 448 449 public Element getFocusElement() { 450 return focusElement; 451 } 452 453 @Override 454 public void setDuration(int duration) { 455 this.duration = duration; 456 } 457 458 @Override 459 public int getDuration() { 460 return duration; 461 } 462 463 @Override 464 public HandlerRegistration addCloseHandler(final CloseHandler<MaterialCutOut> handler) { 465 return addHandler(handler, CloseEvent.getType()); 466 } 467 468 @Override 469 public HandlerRegistration addOpenHandler(OpenHandler<MaterialCutOut> handler) { 470 return addHandler(handler, OpenEvent.getType()); 471 } 472}