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.client.base; 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.event.dom.client.ClickEvent; 027import com.google.gwt.event.shared.HandlerRegistration; 028import com.google.gwt.user.client.Window; 029import com.google.gwt.user.client.ui.RootPanel; 030import com.google.gwt.user.client.ui.Widget; 031import gwt.material.design.client.base.helper.DOMHelper; 032import gwt.material.design.client.base.mixin.StyleMixin; 033import gwt.material.design.client.constants.*; 034import gwt.material.design.client.events.*; 035import gwt.material.design.client.js.JsMaterialElement; 036import gwt.material.design.client.js.JsSideNavOptions; 037import gwt.material.design.client.ui.*; 038import gwt.material.design.client.ui.html.ListItem; 039 040import static gwt.material.design.client.js.JsMaterialElement.$; 041 042//@formatter:off 043 044/** 045 * AbstractSideNav handles the creation and Ui logic for 046 * different sidenavs, you can easily setup any kind of logic for 047 * you sidenav behaviour in {@link AbstractSideNav#setup()}. 048 * 049 * @author kevzlou7979 050 */ 051//@formatter:on 052public abstract class AbstractSideNav extends MaterialWidget implements JsLoader, HasSelectables, HasInOutDurationTransition, HasSideNavHandlers { 053 054 protected int width = 240; 055 protected int inDuration = 400; 056 protected int outDuration = 200; 057 protected boolean open; 058 protected boolean closeOnClick; 059 protected boolean alwaysShowActivator = true; 060 protected boolean allowBodyScroll = true; 061 protected Edge edge = Edge.LEFT; 062 protected Boolean showOnAttach; 063 protected Element activator; 064 065 private StyleMixin<MaterialSideNav> typeMixin; 066 067 public AbstractSideNav() { 068 super(Document.get().createULElement(), CssName.SIDE_NAV); 069 070 } 071 072 public AbstractSideNav(final Widget... widgets) { 073 this(); 074 for (final Widget w : widgets) { 075 add(w); 076 } 077 } 078 079 public AbstractSideNav(SideNavType type) { 080 this(); 081 setType(type); 082 } 083 084 @Override 085 protected void onLoad() { 086 super.onLoad(); 087 088 load(); 089 090 if (showOnAttach != null) { 091 // Ensure the side nav starts closed 092 $(activator).trigger("menu-in", null); 093 094 if (showOnAttach) { 095 Scheduler.get().scheduleDeferred(() -> { 096 // We are ignoring cases with mobile 097 if (Window.getClientWidth() > 960) { 098 show(); 099 } 100 }); 101 } 102 } else { 103 if (Window.getClientWidth() > 960) { 104 $(activator).trigger("menu-out", null); 105 } 106 } 107 } 108 109 @Override 110 protected void onUnload() { 111 super.onUnload(); 112 113 unload(); 114 } 115 116 public Widget wrap(Widget child) { 117 if (child instanceof MaterialImage) { 118 child.getElement().getStyle().setProperty("border", "1px solid #e9e9e9"); 119 child.getElement().getStyle().setProperty("textAlign", "center"); 120 } 121 122 // Check whether the widget is not selectable by default 123 boolean isNotSelectable = false; 124 if (child instanceof MaterialWidget) { 125 MaterialWidget widget = (MaterialWidget) child; 126 if (widget.getInitialClasses() != null) { 127 if (widget.getInitialClasses().length > 0) { 128 if (child instanceof HasNoSideNavSelection) { 129 isNotSelectable = true; 130 } 131 } 132 } 133 } 134 135 if (!(child instanceof ListItem)) { 136 // Direct list item not collapsible 137 final ListItem listItem = new ListItem(); 138 if (child instanceof MaterialCollapsible) { 139 listItem.getElement().getStyle().setBackgroundColor("transparent"); 140 } 141 if (child instanceof HasWaves) { 142 listItem.setWaves(((HasWaves) child).getWaves()); 143 ((HasWaves) child).setWaves(null); 144 } 145 if (child instanceof HasNoSideNavSelection) { 146 super.add(child); 147 } else { 148 listItem.add(child); 149 child = listItem; 150 } 151 } 152 153 // Collapsible and Side Porfile should not be selectable 154 final Widget finalChild = child; 155 if (!isNotSelectable) { 156 // Active click handler 157 registerHandler(finalChild.addDomHandler(event -> { 158 clearActive(); 159 finalChild.addStyleName(CssName.ACTIVE); 160 }, ClickEvent.getType())); 161 } 162 child.getElement().getStyle().setDisplay(Style.Display.BLOCK); 163 return child; 164 } 165 166 @Override 167 public void add(Widget child) { 168 super.add(wrap(child)); 169 } 170 171 @Override 172 protected void insert(Widget child, com.google.gwt.user.client.Element container, int beforeIndex, boolean domInsert) { 173 super.insert(wrap(child), container, beforeIndex, domInsert); 174 } 175 176 protected void pushElement(Element element, int value) { 177 applyTransition($(element).asElement()); 178 if (getEdge() == Edge.RIGHT) { 179 $(element).css("paddingRight", value + "px"); 180 } else { 181 $(element).css("paddingLeft", value + "px"); 182 } 183 184 } 185 186 protected void pushElementMargin(Element element, int value) { 187 applyTransition($(element).asElement()); 188 if (getEdge() == Edge.LEFT) { 189 $(element).css("margin-left", value + "px"); 190 } else { 191 $(element).css("margin-right", value + "px"); 192 } 193 } 194 195 protected void applyBodyScroll() { 196 if (isAllowBodyScroll()) { 197 $("header").css("width", "100%"); 198 $("header").css("position", "fixed"); 199 $("header").css("zIndex", "997"); 200 $(getElement()).css("position", "fixed"); 201 } 202 } 203 204 protected void applyTransition(Element element) { 205 applyTransition(element, "all"); 206 } 207 208 protected void applyTransition(Element element, String property) { 209 int duration; 210 if (isOpen()) { 211 duration = inDuration; 212 } else { 213 duration = outDuration; 214 } 215 if (element != null) { 216 setTransition(new TransitionConfig(element, duration, 0, property, "cubic-bezier(0, 0, 0.2, 1)")); 217 } 218 } 219 220 @Override 221 public void clearActive() { 222 clearActiveClass(this); 223 ClearActiveEvent.fire(this); 224 } 225 226 public void setActive(int index) { 227 clearActive(); 228 getWidget(index).addStyleName(CssName.ACTIVE); 229 } 230 231 @Override 232 public void load() { 233 load(true); 234 } 235 236 @Override 237 public void unload() { 238 $("#sidenav-overlay").remove(); 239 activator = null; 240 } 241 242 /** 243 * Reinitialize the side nav configurations when changing properties. 244 */ 245 @Override 246 public void reload() { 247 unload(); 248 load(false); 249 } 250 251 protected void load(boolean strict) { 252 try { 253 activator = DOMHelper.getElementByAttribute("data-activates", getId()); 254 getNavMenu().setShowOn(ShowOn.SHOW_ON_MED_DOWN); 255 if (alwaysShowActivator && !getTypeMixin().getStyle().equals(SideNavType.FIXED.getCssName())) { 256 getNavMenu().setShowOn(ShowOn.SHOW_ON_LARGE); 257 } else { 258 getNavMenu().setHideOn(HideOn.HIDE_ON_LARGE); 259 } 260 getNavMenu().removeStyleName(CssName.NAVMENU_PERMANENT); 261 } catch (Exception ex) { 262 if (strict) { 263 throw new IllegalArgumentException( 264 "Could not setup MaterialSideNav please ensure you have " + 265 "MaterialNavBar with an activator setup to match this widgets id.", ex); 266 } 267 } 268 269 setup(); 270 271 JsSideNavOptions options = new JsSideNavOptions(); 272 options.menuWidth = width; 273 options.edge = edge != null ? edge.getCssName() : null; 274 options.closeOnClick = closeOnClick; 275 276 JsMaterialElement element = $(activator); 277 element.sideNav(options); 278 279 element.off("side-nav-closing"); 280 element.on("side-nav-closing", e1 -> { 281 onClosing(); 282 return true; 283 }); 284 285 element.off("side-nav-closed"); 286 element.on("side-nav-closed", e1 -> { 287 onClosed(); 288 return true; 289 }); 290 291 element.off("side-nav-opening"); 292 element.on("side-nav-opening", e1 -> { 293 onOpening(); 294 return true; 295 }); 296 297 element.off("side-nav-opened"); 298 element.on("side-nav-opened", e1 -> { 299 onOpened(); 300 return true; 301 }); 302 } 303 304 /** 305 * Override the type of your sidenav. 306 * Used by {@link MaterialSideNavDrawer}, {@link MaterialSideNavCard}, {@link MaterialSideNavMini}, {@link MaterialSideNavPush} 307 */ 308 protected abstract void setup(); 309 310 @Override 311 protected void onDetach() { 312 super.onDetach(); 313 getNavMenu().setVisibility(Style.Visibility.HIDDEN); 314 getNavMenu().removeStyleName(ShowOn.SHOW_ON_LARGE.getCssName()); 315 getNavMenu().removeStyleName(ShowOn.SHOW_ON_MED_DOWN.getCssName()); 316 pushElement(getHeader(), 0); 317 pushElement(getMain(), 0); 318 pushElementMargin(getFooter(), 0); 319 } 320 321 @Override 322 protected void onAttach() { 323 super.onAttach(); 324 getNavMenu().setVisibility(Style.Visibility.VISIBLE); 325 } 326 327 protected Element getMain() { 328 return $("main").asElement(); 329 } 330 331 protected Element getHeader() { 332 return $("header").asElement(); 333 } 334 335 protected Element getFooter() { 336 return $("footer").asElement(); 337 } 338 339 @Override 340 public void setWidth(String width) { 341 setWidth(Integer.parseInt(width)); 342 } 343 344 /** 345 * Set the menu's width in pixels. 346 */ 347 public void setWidth(int width) { 348 this.width = width; 349 getElement().getStyle().setWidth(width, Style.Unit.PX); 350 } 351 352 public int getWidth() { 353 return width; 354 } 355 356 public boolean isCloseOnClick() { 357 return closeOnClick; 358 } 359 360 /** 361 * Close the side nav menu when an \<a\> tag is clicked 362 * from inside it. Note that if you want this to work you 363 * must wrap your item within a {@link MaterialLink}. 364 */ 365 public void setCloseOnClick(boolean closeOnClick) { 366 this.closeOnClick = closeOnClick; 367 } 368 369 public Edge getEdge() { 370 return edge; 371 } 372 373 /** 374 * Set which edge of the window the menu should attach to. 375 */ 376 public void setEdge(Edge edge) { 377 this.edge = edge; 378 } 379 380 protected void setType(SideNavType type) { 381 getTypeMixin().setStyle(type.getCssName()); 382 } 383 384 protected boolean isSmall() { 385 return !gwt.material.design.client.js.Window.matchMedia("all and (max-width: 992px)"); 386 } 387 388 protected MaterialWidget getNavMenu() { 389 Element navMenuElement = DOMHelper.getElementByAttribute("data-activates", getId()); 390 if (navMenuElement != null) { 391 return new MaterialWidget(navMenuElement); 392 } 393 return null; 394 } 395 396 protected void onClosing() { 397 open = false; 398 SideNavClosingEvent.fire(this); 399 } 400 401 protected void onClosed() { 402 SideNavClosedEvent.fire(this); 403 } 404 405 protected void onOpening() { 406 open = true; 407 SideNavOpeningEvent.fire(this); 408 } 409 410 protected void onOpened() { 411 if (allowBodyScroll) { 412 RootPanel.getBodyElement().getStyle().clearOverflow(); 413 } 414 SideNavOpenedEvent.fire(this); 415 } 416 417 /** 418 * Hide the overlay menu. 419 */ 420 public void hideOverlay() { 421 $("#sidenav-overlay").remove(); 422 } 423 424 /** 425 * Show the sidenav using the activator element 426 */ 427 public void show() { 428 $("#sidenav-overlay").remove(); 429 $(activator).sideNav("show"); 430 } 431 432 /** 433 * Hide the sidenav using the activator element 434 */ 435 public void hide() { 436 $(activator).sideNav("hide"); 437 } 438 439 public boolean isOpen() { 440 return open; 441 } 442 443 /** 444 * Will the body have scroll capability 445 * while the menu is open. 446 */ 447 public boolean isAllowBodyScroll() { 448 return allowBodyScroll; 449 } 450 451 /** 452 * Allow the body to maintain its scroll capability 453 * while the menu is visible. 454 */ 455 public void setAllowBodyScroll(boolean allowBodyScroll) { 456 this.allowBodyScroll = allowBodyScroll; 457 } 458 459 /** 460 * Will the activator always be shown. 461 */ 462 public boolean isAlwaysShowActivator() { 463 return alwaysShowActivator; 464 } 465 466 /** 467 * Disable the hiding of your activator element. 468 */ 469 public void setAlwaysShowActivator(boolean alwaysShowActivator) { 470 this.alwaysShowActivator = alwaysShowActivator; 471 } 472 473 /** 474 * Will the menu forcefully show on attachment. 475 */ 476 public boolean isShowOnAttach() { 477 return showOnAttach != null && showOnAttach; 478 } 479 480 /** 481 * Show the menu upon attachment.<br> 482 * Note that you shouldn't apply this setting if you want your side nav to appear static. 483 * otherwise when set to <code>true</code> will slide in from the left. 484 */ 485 public void setShowOnAttach(boolean showOnAttach) { 486 this.showOnAttach = showOnAttach; 487 } 488 489 @Override 490 public void setEnabled(boolean enabled) { 491 getEnabledMixin().setEnabled(this, enabled); 492 } 493 494 @Override 495 public void setInDuration(int inDuration) { 496 this.inDuration = inDuration; 497 } 498 499 @Override 500 public int getInDuration() { 501 return inDuration; 502 } 503 504 @Override 505 public void setOutDuration(int outDuration) { 506 this.outDuration = outDuration; 507 } 508 509 @Override 510 public int getOutDuration() { 511 return outDuration; 512 } 513 514 public Element getActivator() { 515 return activator; 516 } 517 518 @Override 519 public HandlerRegistration addOpeningHandler(SideNavOpeningEvent.SideNavOpeningHandler handler) { 520 return addHandler(handler, SideNavOpeningEvent.TYPE); 521 } 522 523 @Override 524 public HandlerRegistration addOpenedHandler(SideNavOpenedEvent.SideNavOpenedHandler handler) { 525 return addHandler(handler, SideNavOpenedEvent.TYPE); 526 } 527 528 @Override 529 public HandlerRegistration addClosingHandler(SideNavClosingEvent.SideNavClosingHandler handler) { 530 return addHandler(handler, SideNavClosingEvent.TYPE); 531 } 532 533 @Override 534 public HandlerRegistration addClosedHandler(SideNavClosedEvent.SideNavClosedHandler handler) { 535 return addHandler(handler, SideNavClosedEvent.TYPE); 536 } 537 538 protected StyleMixin<MaterialSideNav> getTypeMixin() { 539 if (typeMixin == null) { 540 typeMixin = new StyleMixin(this); 541 } 542 return typeMixin; 543 } 544}