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}