001package gwt.material.design.client.ui;
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 com.google.gwt.core.client.Scheduler;
024import com.google.gwt.core.client.Scheduler.ScheduledCommand;
025import com.google.gwt.dom.client.Document;
026import com.google.gwt.dom.client.Element;
027import com.google.gwt.dom.client.Style;
028import com.google.gwt.dom.client.Style.Unit;
029import com.google.gwt.event.dom.client.ClickEvent;
030import com.google.gwt.event.dom.client.ClickHandler;
031import com.google.gwt.uibinder.client.UiConstructor;
032import com.google.gwt.user.client.Timer;
033import com.google.gwt.user.client.Window;
034import com.google.gwt.user.client.ui.RootPanel;
035import com.google.gwt.user.client.ui.Widget;
036import com.google.web.bindery.event.shared.HandlerRegistration;
037import gwt.material.design.client.base.HasSelectables;
038import gwt.material.design.client.base.HasType;
039import gwt.material.design.client.base.HasWaves;
040import gwt.material.design.client.base.MaterialWidget;
041import gwt.material.design.client.base.helper.DOMHelper;
042import gwt.material.design.client.base.mixin.CssTypeMixin;
043import gwt.material.design.client.base.mixin.ToggleStyleMixin;
044import gwt.material.design.client.constants.Edge;
045import gwt.material.design.client.constants.SideNavType;
046import gwt.material.design.client.events.*;
047import gwt.material.design.client.events.SideNavClosedEvent.SideNavClosedHandler;
048import gwt.material.design.client.events.SideNavClosingEvent.SideNavClosingHandler;
049import gwt.material.design.client.events.SideNavOpenedEvent.SideNavOpenedHandler;
050import gwt.material.design.client.events.SideNavOpeningEvent.SideNavOpeningHandler;
051import gwt.material.design.client.ui.html.ListItem;
052
053//@formatter:off
054
055/**
056 * SideNav is a material component that gives you a lists of menus and other navigation components.
057 *
058 * <h3>UiBinder Usage:</h3>
059 * <pre>
060 * {@code
061 * <m:MaterialSideNav ui:field="sideNav" width="280" m:id="mysidebar"  type="OPEN" closeOnClick="false">
062 *     <m:MaterialLink href="#about" iconPosition="LEFT" iconType="OUTLINE" text="About" textColor="blue"  />
063 *     <m:MaterialLink href="#gettingStarted" iconPosition="LEFT" iconType="DOWNLOAD" text="Getting Started" textColor="blue"  >
064 * </m:MaterialSideNav>
065 * }
066 * </pre>
067 *
068 * @author kevzlou7979
069 * @author Ben Dol
070 * @see <a href="http://gwt-material-demo.herokuapp.com/#sidenav">Material SideNav</a>
071 */
072//@formatter:on
073public class MaterialSideNav extends MaterialWidget implements HasType<SideNavType>, HasSelectables {
074
075    private int width = 240;
076    private Edge edge = Edge.LEFT;
077    private boolean closeOnClick = false;
078    private boolean alwaysShowActivator = false;
079    private boolean allowBodyScroll = false;
080    private boolean showOnAttach = false;
081    private boolean open;
082
083    private Element activator;
084
085    private final CssTypeMixin<SideNavType, MaterialSideNav> typeMixin = new CssTypeMixin<>(this);
086    private final ToggleStyleMixin<MaterialSideNav> fixedMixin = new ToggleStyleMixin<>(this, "fixed");
087
088    /**
089     * Container for App Toolbar and App Sidebar , contains Material Links,
090     * Icons or any other material components.
091     */
092    public MaterialSideNav() {
093        super(Document.get().createULElement(), "side-nav");
094    }
095
096    /**
097     *  Creates a list and adds the given widgets.
098     */
099    public MaterialSideNav(final Widget... widgets) {
100        this();
101        for (final Widget w : widgets) {
102            add(w);
103        }
104    }
105
106    @UiConstructor
107    public MaterialSideNav(SideNavType type) {
108        this();
109        setType(type);
110    }
111
112    @Override
113    public void onLoad() {
114        super.onLoad();
115
116        // Initialize the side nav
117        initialize();
118
119        if(showOnAttach) {
120            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
121                @Override
122                public void execute() {
123                    if(Window.getClientWidth() > 960) {
124                        show();
125                    }
126                }
127            });
128        }
129    }
130
131    /**
132     * This handler will be triggered when the side nav starts opening.
133     */
134    public HandlerRegistration addOpeningHandler(SideNavOpeningHandler handler) {
135        return addHandler(handler, SideNavOpeningEvent.TYPE);
136    }
137
138    /**
139     * This handler will be triggered when the side nav is opened.
140     */
141    public HandlerRegistration addOpenedHandler(SideNavOpenedHandler handler) {
142        return addHandler(handler, SideNavOpenedEvent.TYPE);
143    }
144
145    /**
146     * This handler will be triggered when the side nav starts closing.
147     */
148    public HandlerRegistration addClosingHandler(SideNavClosingHandler handler) {
149        return addHandler(handler, SideNavClosingEvent.TYPE);
150    }
151
152    /**
153     * This handler will be triggered when the side nav is closed.
154     */
155    public HandlerRegistration addClosedHandler(SideNavClosedHandler handler) {
156        return addHandler(handler, SideNavClosedEvent.TYPE);
157    }
158
159    public Widget wrap(Widget child) {
160        if(child instanceof MaterialImage) {
161            child.getElement().getStyle().setProperty("border", "1px solid #e9e9e9");
162            child.getElement().getStyle().setProperty("textAlign", "center");
163        }
164
165        // Check whether the widget is not selectable by default
166        boolean isNotSelectable = false;
167        if(child instanceof MaterialWidget) {
168            MaterialWidget widget = (MaterialWidget) child;
169            if (widget.getInitialClasses() != null) {
170                if (widget.getInitialClasses().length > 0) {
171                    String initialClass = widget.getInitialClasses()[0];
172                    if(initialClass.contains("side-profile") || initialClass.contains("collapsible")) {
173                        isNotSelectable = true;
174                    }
175                }
176            }
177        }
178
179        if(!(child instanceof ListItem)) {
180            // Direct list item not collapsible
181            final ListItem listItem = new ListItem();
182            if(child instanceof MaterialCollapsible) {
183                listItem.getElement().getStyle().setBackgroundColor("transparent");
184            }
185            if(child instanceof HasWaves) {
186                listItem.setWaves(((HasWaves) child).getWaves());
187                ((HasWaves) child).setWaves(null);
188            }
189            listItem.add(child);
190
191            child = listItem;
192        }
193
194        // Collapsible and Side Porfile should not be selectable
195        final Widget finalChild = child;
196        if(!isNotSelectable) {
197            // Active click handler
198            finalChild.addDomHandler(new ClickHandler() {
199                @Override
200                public void onClick(ClickEvent event) {
201                    clearActive();
202                    finalChild.addStyleName("active");
203                }
204            }, ClickEvent.getType());
205        }
206        child.getElement().getStyle().setDisplay(Style.Display.BLOCK);
207        return child;
208    }
209
210    @Override
211    public void add(Widget child) {
212        super.add(wrap(child));
213    }
214
215    @Override
216    protected void insert(Widget child, com.google.gwt.user.client.Element container, int beforeIndex, boolean domInsert) {
217        super.insert(wrap(child), container, beforeIndex, domInsert);
218    }
219
220    @Override
221    public void setWidth(String width) {
222        setWidth(Integer.parseInt(width));
223    }
224
225    /**
226     * Set the menu's width in pixels.
227     */
228    public void setWidth(int width) {
229        this.width = width;
230        getElement().getStyle().setWidth(width, Unit.PX);
231    }
232
233    public int getWidth() {
234        return width;
235    }
236
237    public boolean isCloseOnClick() {
238        return closeOnClick;
239    }
240
241    /**
242     * Close the side nav menu when an \<a\> tag is clicked
243     * from inside it. Note that if you want this to work you
244     * must wrap your item within a {@link MaterialLink}.
245     */
246    public void setCloseOnClick(boolean closeOnClick) {
247        this.closeOnClick = closeOnClick;
248    }
249
250    public Edge getEdge() {
251        return edge;
252    }
253
254    /**
255     * Set which edge of the window the menu should attach to.
256     */
257    public void setEdge(Edge edge) {
258        this.edge = edge;
259    }
260
261    public boolean isFixed() {
262        return fixedMixin.isOn();
263    }
264
265    /**
266     * Fixed determines its display state on loading
267     * (fixed being visible on load).
268     */
269    public void setFixed(boolean fixed) {
270        fixedMixin.setOn(fixed);
271    }
272
273    /**
274     * Define the menu's type specification.
275     */
276    public void setType(SideNavType type) {
277        typeMixin.setType(type);
278    }
279
280    @Override
281    public SideNavType getType() {
282        return typeMixin.getType();
283    }
284
285    protected void processType(SideNavType type) {
286        if(activator != null && type != null) {
287            addStyleName(type.getCssName());
288            switch (type) {
289                case MINI:
290                    setWidth(64);
291                    break;
292                case CARD:
293                    new Timer() {
294                        @Override
295                        public void run() {
296                            if(isSmall()) { show(); }
297                        }}.schedule(500);
298                    break;
299                case PUSH:
300                    applyPushType(getElement(), activator, width);
301                    break;
302            }
303        }
304    }
305
306    protected native boolean isSmall() /*-{
307        var mq = $wnd.window.matchMedia('all and (max-width: 992px)');
308        if(!mq.matches) {
309            return true;
310        }
311        return false;
312    }-*/;
313
314    /**
315     * Push the header, footer, and main to the right part when Close type is applied.
316     */
317    protected native void applyPushType(Element element, Element activator, double width) /*-{
318        var that = this;
319
320        $wnd.jQuery($wnd.window).off("resize");
321        $wnd.jQuery($wnd.window).resize(function() {
322            var toggle = that.@gwt.material.design.client.ui.MaterialSideNav::open;
323            that.@gwt.material.design.client.ui.MaterialSideNav::pushElements(*)(toggle, width);
324        });
325    }-*/;
326
327    protected native void pushElements(boolean toggle, int width) /*-{
328        var _width = 0;
329        var _duration = 200;
330
331        var mq = $wnd.window.matchMedia('all and (max-width: 992px)');
332        if(!mq.matches) {
333            if(toggle) {
334                _width = width;
335                _duration = 300;
336            }
337
338            applyTransition($wnd.jQuery('header'), _width);
339            applyTransition($wnd.jQuery('main'), _width);
340            applyTransition($wnd.jQuery('footer'), _width);
341
342            function applyTransition(elem, _width) {
343                $wnd.jQuery(elem).css('transition', _duration + 'ms');
344                $wnd.jQuery(elem).css('-moz-transition', _duration + 'ms');
345                $wnd.jQuery(elem).css('-webkit-transition', _duration + 'ms');
346                $wnd.jQuery(elem).css('margin-left', _width);
347            }
348        }
349        this.@gwt.material.design.client.ui.MaterialSideNav::onPush(*)(toggle, _width, _duration);
350    }-*/;
351
352    protected void onPush(boolean toggle, int width, int duration) {
353        SideNavPushEvent.fire(this, getElement(), activator, toggle, width, duration);
354    }
355
356    @Override
357    public void clearActive() {
358        clearActiveClass(this);
359        ClearActiveEvent.fire(this);
360    }
361
362    /**
363     * reinitialize the side nav configurations when changing
364     * properties.
365     */
366    public void reinitialize() {
367        activator = null;
368        initialize(false);
369    }
370
371    protected void initialize() {
372        initialize(true);
373    }
374
375    protected void initialize(boolean strict) {
376        if(activator == null) {
377            activator = DOMHelper.getElementByAttribute("data-activates", getId());
378            if (activator != null) {
379                SideNavType type = getType();
380                processType(type);
381
382                initialize(activator, width, closeOnClick, edge.getCssName());
383
384                if(alwaysShowActivator || !isFixed()) {
385                    String style = activator.getAttribute("style");
386                    activator.setAttribute("style", style + "; display: block !important");
387                    activator.removeClassName("navmenu-permanent");
388                }
389            } else if(strict) {
390                throw new RuntimeException("Cannot find an activator for the MaterialSideNav, " +
391                        "please ensure you have a MaterialNavBar with an activator setup to match " +
392                        "this widgets id.");
393            }
394        }
395    }
396
397    protected native void initialize(Element e, int width, boolean closeOnClick, String edge)/*-{
398        var that = this;
399        var $e = $wnd.jQuery(e);
400        $wnd.jQuery(e).sideNav({
401            menuWidth: width,
402            edge: edge,
403            closeOnClick: closeOnClick
404        });
405
406        $e.off("side-nav-closing");
407        $e.on("side-nav-closing", function() {
408            that.@gwt.material.design.client.ui.MaterialSideNav::onClosing()();
409        });
410
411        $e.off("side-nav-closed");
412        $e.on("side-nav-closed", function() {
413            that.@gwt.material.design.client.ui.MaterialSideNav::onClosed()();
414        });
415
416        $e.off("side-nav-opening");
417        $e.on("side-nav-opening", function() {
418            that.@gwt.material.design.client.ui.MaterialSideNav::onOpening()();
419        });
420
421        $e.off("side-nav-opened");
422        $e.on("side-nav-opened", function() {
423            that.@gwt.material.design.client.ui.MaterialSideNav::onOpened()();
424        });
425    }-*/;
426
427    protected void onClosing() {
428        open = false;
429        if(getType().equals(SideNavType.PUSH)) {
430            pushElements(false, width);
431        }
432
433        SideNavClosingEvent.fire(this);
434    }
435
436    protected void onClosed() {
437        SideNavClosedEvent.fire(this);
438    }
439
440    protected void onOpening() {
441        open = true;
442        if(getType().equals(SideNavType.PUSH)) {
443            pushElements(true, width);
444        }
445
446        SideNavOpeningEvent.fire(this);
447    }
448
449    protected void onOpened() {
450        if(allowBodyScroll) {
451            RootPanel.getBodyElement().getStyle().clearOverflow();
452        }
453
454        SideNavOpenedEvent.fire(this);
455    }
456
457    /**
458     * Hide the overlay menu.
459     */
460    public native void hideOverlay()/*-{
461        $wnd.jQuery(document).ready(function() {
462            $wnd.jQuery('#sidenav-overlay').remove();
463        })
464    }-*/;
465
466    /**
467     * Show the sidenav.
468     */
469    protected native void show(Element e)/*-{
470        $wnd.jQuery(document).ready(function() {
471            $wnd.jQuery(e).sideNav('show');
472        });
473    }-*/;
474
475    /**
476     * Hide the sidenav.
477     */
478    protected native void hide(Element e)/*-{
479        $wnd.jQuery(document).ready(function() {
480            $wnd.jQuery(e).sideNav('hide');
481        });
482    }-*/;
483
484    /**
485     * Show the sidenav using the activator element
486     */
487    public void show() {
488        show(activator);
489    }
490
491    /**
492     * Hide the sidenav using the activator element
493     */
494    public void hide() {
495        hide(activator);
496    }
497
498    public boolean isOpen() {
499        return open;
500    }
501
502    /**
503     * Will the body have scroll capability
504     * while the menu is open.
505     */
506    public boolean isAllowBodyScroll() {
507        return allowBodyScroll;
508    }
509
510    /**
511     * Allow the body to maintain its scroll capability
512     * while the menu is visible.
513     */
514    public void setAllowBodyScroll(boolean allowBodyScroll) {
515        this.allowBodyScroll = allowBodyScroll;
516    }
517
518    /**
519     * Will the activator always be shown.
520     */
521    public boolean isAlwaysShowActivator() {
522        return alwaysShowActivator;
523    }
524
525    /**
526     * Disable the hiding of your activator element.
527     */
528    public void setAlwaysShowActivator(boolean alwaysShowActivator) {
529        this.alwaysShowActivator = alwaysShowActivator;
530    }
531
532    /**
533     * Will the menu forcefully show on attachment.
534     */
535    public boolean isShowOnAttach() {
536        return showOnAttach;
537    }
538
539    /**
540     * Show the menu upon attachment, this isn't always required.
541     * Some menu types will automatically show themselves by default.
542     */
543    public void setShowOnAttach(boolean showOnAttach) {
544        this.showOnAttach = showOnAttach;
545    }
546}