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.ui;
021
022import com.google.gwt.core.client.GWT;
023import com.google.gwt.dom.client.Element;
024import com.google.gwt.dom.client.Style;
025import com.google.gwt.event.dom.client.ClickEvent;
026import com.google.gwt.event.dom.client.DomEvent;
027import com.google.gwt.event.logical.shared.HasSelectionHandlers;
028import com.google.gwt.event.logical.shared.SelectionEvent;
029import com.google.gwt.event.logical.shared.SelectionHandler;
030import com.google.gwt.event.shared.HandlerRegistration;
031import com.google.gwt.uibinder.client.UiConstructor;
032import com.google.gwt.user.client.DOM;
033import com.google.gwt.user.client.ui.HasEnabled;
034import com.google.gwt.user.client.ui.UIObject;
035import com.google.gwt.user.client.ui.Widget;
036import gwt.material.design.client.base.*;
037import gwt.material.design.client.base.helper.DOMHelper;
038import gwt.material.design.client.constants.Alignment;
039import gwt.material.design.client.constants.CssName;
040import gwt.material.design.client.js.JsDropdownOptions;
041import gwt.material.design.client.ui.html.ListItem;
042import gwt.material.design.client.ui.html.UnorderedList;
043
044import java.util.List;
045
046import static gwt.material.design.client.js.JsMaterialElement.$;
047
048//@formatter:off
049
050/**
051 * You can add dropdown easily by specifying it's item
052 * content and add a UiHandler on it to implement any event.
053 * <p>
054 * <h3>UiBinder Usage:</h3>
055 * <pre>
056 * {@code
057 * <m:MaterialDropDown>
058 *   <m:MaterialLink text="First" />
059 *   <m:MaterialLink text="Second" />
060 *   <m:MaterialLink text="Third" />
061 * </m:MaterialDropDown>
062 * }
063 * </pre>
064 *
065 * @author kevzlou7979
066 * @author Ben Dol
067 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#dropdown">Material DropDown</a>
068 * @see <a href="https://material.io/guidelines/components/menus.html#">Material Design Specification</a>
069 */
070//@formatter:on
071public class MaterialDropDown extends UnorderedList implements JsLoader, HasSelectionHandlers<Widget>, HasInOutDurationTransition {
072
073    private String activator;
074    private Element activatorElement;
075    private JsDropdownOptions options = new JsDropdownOptions();
076
077    public MaterialDropDown() {
078        setInitialClasses(CssName.DROPDOWN_CONTENT);
079        setId(DOM.createUniqueId());
080    }
081
082    /**
083     * Add a list item selection when button, link, icon button pressed.
084     *
085     * @param activator data-activates attribute name of your dropdown activator.
086     */
087    @UiConstructor
088    public MaterialDropDown(String activator) {
089        this();
090        this.activator = activator;
091        getElement().setId(this.activator);
092    }
093
094    public MaterialDropDown(Element activatorElement) {
095        this();
096        activatorElement.setAttribute("data-activates", getId());
097        this.activatorElement = activatorElement;
098    }
099
100    public MaterialDropDown(UIObject activator) {
101        this(activator.getElement());
102    }
103
104    @Override
105    protected void onLoad() {
106        super.onLoad();
107
108        load();
109
110        // register dropdown item handler
111        registerDropdownItemHandlers();
112    }
113
114    protected void registerDropdownItemHandlers() {
115        getChildren().forEach(widget -> {
116            if (widget instanceof ListItem) {
117                ListItem item = (ListItem) widget;
118                if (item.getWidget(0) instanceof MaterialWidget) {
119                    MaterialWidget child = (MaterialWidget) item.getWidget(0);
120                    registerHandler(child.addDomHandler(event -> {
121                        SelectionEvent.fire(MaterialDropDown.this, child);
122                    }, ClickEvent.getType()));
123                }
124            }
125        });
126    }
127
128    @Override
129    protected void onUnload() {
130        super.onUnload();
131
132        unload();
133    }
134
135    @Override
136    public void load() {
137        Widget parent = getParent();
138        if (parent instanceof HasActivates) {
139            String uid = DOM.createUniqueId();
140            ((HasActivates) parent).setActivates(uid);
141            setId(uid);
142            activatorElement = parent.getElement();
143        } else if (activatorElement == null) {
144            activatorElement = DOMHelper.getElementByAttribute("data-activates", activator);
145            if (activatorElement == null) {
146                GWT.log("There is no activator element with id: '" + activator + "' in the DOM, " +
147                        "cannot instantiate MaterialDropDown without a data-activates.", new IllegalStateException());
148            }
149        }
150
151        $(activatorElement).dropdown(options);
152    }
153
154    @Override
155    public void unload() {
156        $(activatorElement).dropdown("remove");
157    }
158
159    @Override
160    public void reload() {
161        unload();
162        load();
163    }
164
165    @Override
166    public void add(final Widget child) {
167        String tagName = child.getElement().getTagName();
168        if (child instanceof ListItem || tagName.toLowerCase().startsWith("li")) {
169            child.getElement().getStyle().setDisplay(Style.Display.BLOCK);
170            add(child, (Element) getElement());
171        } else {
172            ListItem li = new ListItem(child);
173            // Checks if there are sub dropdown components
174            if (child instanceof MaterialLink) {
175                MaterialLink link = (MaterialLink) child;
176                for (int i = 0; i < link.getWidgetCount(); i++) {
177                    if (link.getWidget(i) instanceof MaterialDropDown) {
178                        registerHandler(link.addClickHandler(DomEvent::stopPropagation));
179                        link.stopTouchStartEvent();
180                    }
181                }
182            }
183
184            if (child instanceof HasWaves) {
185                li.setWaves(((HasWaves) child).getWaves());
186                ((HasWaves) child).setWaves(null);
187            }
188            li.getElement().getStyle().setDisplay(Style.Display.BLOCK);
189            add(li, (Element) getElement());
190        }
191    }
192
193    @Override
194    public void setInDuration(int durationMillis) {
195        options.inDuration = durationMillis;
196    }
197
198    @Override
199    public int getInDuration() {
200        return options.inDuration;
201    }
202
203    @Override
204    public void setOutDuration(int durationMillis) {
205        options.outDuration = durationMillis;
206    }
207
208    @Override
209    public int getOutDuration() {
210        return options.outDuration;
211    }
212
213    /**
214     * If true, constrainWidth to the size of the dropdown activator. Default: true
215     */
216    public void setConstrainWidth(boolean constrainWidth) {
217        options.constrain_width = constrainWidth;
218    }
219
220    public boolean isConstrainWidth() {
221        return options.constrain_width;
222    }
223
224    /**
225     * If true, the dropdown will open on hover. Default: false
226     */
227    public void setHover(boolean hover) {
228        options.hover = hover;
229    }
230
231    public boolean isHover() {
232        return options.hover;
233    }
234
235    /**
236     * This defines the spacing from the aligned edge. Default: 0
237     */
238    public void setGutter(int gutter) {
239        options.gutter = gutter;
240    }
241
242    public int getGutter() {
243        return options.gutter;
244    }
245
246    /**
247     * If true, the dropdown will show below the activator. Default: false
248     */
249    public void setBelowOrigin(boolean belowOrigin) {
250        options.belowOrigin = belowOrigin;
251    }
252
253    public boolean isBelowOrigin() {
254        return options.belowOrigin;
255    }
256
257    /**
258     * Defines the edge the menu is aligned to. Default: 'left'
259     */
260    public void setAlignment(Alignment alignment) {
261        options.alignment = alignment.getCssName();
262    }
263
264    public Alignment getAlignment() {
265        return Alignment.fromStyleName(options.alignment);
266    }
267
268    /**
269     * Get the unique activator set by material widget e.g links, icons, buttons to trigger the dropdown.
270     */
271    public String getActivator() {
272        return activator;
273    }
274
275    /**
276     * Set the unique activator of each dropdown component and it must be unique
277     */
278    public void setActivator(String activator) {
279        this.activator = activator;
280        setId(activator);
281    }
282
283    public List<Widget> getItems() {
284        return getChildrenList();
285    }
286
287    public Element getActivatorElement() {
288        return activatorElement;
289    }
290
291    @Override
292    public HandlerRegistration addSelectionHandler(final SelectionHandler<Widget> handler) {
293        return addHandler((SelectionHandler<Widget>) event -> {
294            Widget widget = event.getSelectedItem();
295            if (widget instanceof HasEnabled && ((HasEnabled) widget).isEnabled() && isEnabled()) {
296                handler.onSelection(event);
297            }
298        }, SelectionEvent.getType());
299    }
300}