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