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.getWidgetCount() > 0) {
119                    if (item.getWidget(0) instanceof MaterialWidget) {
120                        MaterialWidget child = (MaterialWidget) item.getWidget(0);
121                        registerHandler(child.addDomHandler(event -> {
122                            SelectionEvent.fire(MaterialDropDown.this, child);
123                        }, ClickEvent.getType()));
124                    }
125                }
126            }
127        });
128    }
129
130    @Override
131    protected void onUnload() {
132        super.onUnload();
133
134        unload();
135    }
136
137    @Override
138    public void load() {
139        Widget parent = getParent();
140        if (parent instanceof HasActivates) {
141            String uid = DOM.createUniqueId();
142            ((HasActivates) parent).setActivates(uid);
143            setId(uid);
144            activatorElement = parent.getElement();
145        } else if (activatorElement == null) {
146            activatorElement = DOMHelper.getElementByAttribute("data-activates", activator);
147            if (activatorElement == null) {
148                GWT.log("There is no activator element with id: '" + activator + "' in the DOM, " +
149                        "cannot instantiate MaterialDropDown without a data-activates.", new IllegalStateException());
150            }
151        }
152
153        $(activatorElement).dropdown(options);
154    }
155
156    @Override
157    public void unload() {
158        $(activatorElement).dropdown("remove");
159    }
160
161    @Override
162    public void reload() {
163        unload();
164        load();
165    }
166
167    @Override
168    public void add(final Widget child) {
169        String tagName = child.getElement().getTagName();
170        if (child instanceof ListItem || tagName.toLowerCase().startsWith("li")) {
171            child.getElement().getStyle().setDisplay(Style.Display.BLOCK);
172            add(child, (Element) getElement());
173        } else {
174            ListItem li = new ListItem(child);
175            // Checks if there are sub dropdown components
176            if (child instanceof MaterialLink) {
177                MaterialLink link = (MaterialLink) child;
178                for (int i = 0; i < link.getWidgetCount(); i++) {
179                    if (link.getWidget(i) instanceof MaterialDropDown) {
180                        registerHandler(link.addClickHandler(DomEvent::stopPropagation));
181                        link.stopTouchStartEvent();
182                    }
183                }
184            }
185
186            if (child instanceof HasWaves) {
187                li.setWaves(((HasWaves) child).getWaves());
188                ((HasWaves) child).setWaves(null);
189            }
190            li.getElement().getStyle().setDisplay(Style.Display.BLOCK);
191            add(li, (Element) getElement());
192        }
193    }
194
195    @Override
196    public void setInDuration(int durationMillis) {
197        options.inDuration = durationMillis;
198    }
199
200    @Override
201    public int getInDuration() {
202        return options.inDuration;
203    }
204
205    @Override
206    public void setOutDuration(int durationMillis) {
207        options.outDuration = durationMillis;
208    }
209
210    @Override
211    public int getOutDuration() {
212        return options.outDuration;
213    }
214
215    /**
216     * If true, constrainWidth to the size of the dropdown activator. Default: true
217     */
218    public void setConstrainWidth(boolean constrainWidth) {
219        options.constrain_width = constrainWidth;
220    }
221
222    public boolean isConstrainWidth() {
223        return options.constrain_width;
224    }
225
226    /**
227     * If true, the dropdown will open on hover. Default: false
228     */
229    public void setHover(boolean hover) {
230        options.hover = hover;
231    }
232
233    public boolean isHover() {
234        return options.hover;
235    }
236
237    /**
238     * This defines the spacing from the aligned edge. Default: 0
239     */
240    public void setGutter(int gutter) {
241        options.gutter = gutter;
242    }
243
244    public int getGutter() {
245        return options.gutter;
246    }
247
248    /**
249     * If true, the dropdown will show below the activator. Default: false
250     */
251    public void setBelowOrigin(boolean belowOrigin) {
252        options.belowOrigin = belowOrigin;
253    }
254
255    public boolean isBelowOrigin() {
256        return options.belowOrigin;
257    }
258
259    /**
260     * Defines the edge the menu is aligned to. Default: 'left'
261     */
262    public void setAlignment(Alignment alignment) {
263        options.alignment = alignment.getCssName();
264    }
265
266    public Alignment getAlignment() {
267        return Alignment.fromStyleName(options.alignment);
268    }
269
270    /**
271     * Get the unique activator set by material widget e.g links, icons, buttons to trigger the dropdown.
272     */
273    public String getActivator() {
274        return activator;
275    }
276
277    /**
278     * Set the unique activator of each dropdown component and it must be unique
279     */
280    public void setActivator(String activator) {
281        this.activator = activator;
282        setId(activator);
283    }
284
285    public List<Widget> getItems() {
286        return getChildrenList();
287    }
288
289    public Element getActivatorElement() {
290        return activatorElement;
291    }
292
293    @Override
294    public HandlerRegistration addSelectionHandler(final SelectionHandler<Widget> handler) {
295        return addHandler((SelectionHandler<Widget>) event -> {
296            Widget widget = event.getSelectedItem();
297            if (widget instanceof HasEnabled && ((HasEnabled) widget).isEnabled() && isEnabled()) {
298                handler.onSelection(event);
299            }
300        }, SelectionEvent.getType());
301    }
302}