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.dom.client.Element;
023import com.google.gwt.event.logical.shared.AttachEvent;
024import com.google.gwt.user.client.ui.*;
025import gwt.material.design.client.base.HasId;
026import gwt.material.design.client.base.HasPosition;
027import gwt.material.design.client.base.HasReload;
028import gwt.material.design.client.base.JsLoader;
029import gwt.material.design.client.base.helper.EventHelper;
030import gwt.material.design.client.constants.Position;
031import gwt.material.design.client.js.JsTooltipOptions;
032
033import java.util.Iterator;
034import java.util.NoSuchElementException;
035
036import static gwt.material.design.client.js.JsMaterialElement.$;
037
038/**
039 * Basic implementation for the Material Design tooltip.
040 * <h3>UiBinder Example</h3>
041 * <pre>
042 * {@code
043 * <m:MaterialTooltip text="...">
044 *    ...
045 * </b:MaterialTooltip>
046 * }
047 * </pre>
048 *
049 * @author kevzlou7979
050 * @author Ben Dol
051 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#dialogs">Material Tooltip</a>
052 * @see <a href="https://material.io/guidelines/components/tooltips.html">Material Design Specification</a>
053 */
054public class MaterialTooltip implements JsLoader, IsWidget, HasWidgets, HasOneWidget, HasId, HasText, HasPosition, HasReload {
055
056    private String id;
057    private String html;
058    private Widget widget;
059    private JsTooltipOptions options = JsTooltipOptions.create();
060
061    /**
062     * Creates the empty Tooltip
063     */
064    public MaterialTooltip() {
065    }
066
067    /**
068     * Creates the tooltip around this widget
069     *
070     * @param w widget for the tooltip
071     */
072    public MaterialTooltip(final Widget w) {
073        setWidget(w);
074    }
075
076    /**
077     * Creates the tooltip around this widget with given title
078     *
079     * @param w    widget for the tooltip
080     * @param text text for the tooltip
081     */
082    public MaterialTooltip(final Widget w, final String text) {
083        setWidget(w);
084        setText(text);
085    }
086
087    @Override
088    public void load() {
089        $(widget.getElement()).tooltip(options);
090    }
091
092    @Override
093    public void unload() {
094        command("remove");
095    }
096
097    @Override
098    public void reload() {
099        unload();
100        load();
101    }
102
103    /**
104     * Force the Tooltip to be destroyed
105     *
106     * @deprecated use {@link #unload()}
107     */
108    @Deprecated
109    public void remove() {
110        unload();
111    }
112
113    @Override
114    public void clear() {
115        widget = null;
116    }
117
118    @Override
119    public Iterator<Widget> iterator() {
120        // Simple iterator for the widget
121        return new Iterator<Widget>() {
122            boolean hasElement = widget != null;
123            Widget returned = null;
124
125            @Override
126            public boolean hasNext() {
127                return hasElement;
128            }
129
130            @Override
131            public Widget next() {
132                if (!hasElement || (widget == null)) {
133                    throw new NoSuchElementException();
134                }
135                hasElement = false;
136                return (returned = widget);
137            }
138
139            @Override
140            public void remove() {
141                if (returned != null) {
142                    MaterialTooltip.this.remove(returned);
143                }
144            }
145        };
146    }
147
148    @Override
149    public boolean remove(final Widget w) {
150        // Validate.
151        if (widget != w) {
152            return false;
153        }
154
155        // Logical detach.
156        clear();
157        return true;
158    }
159
160    @Override
161    public Widget asWidget() {
162        return widget;
163    }
164
165    @Override
166    public String toString() {
167        return asWidget().toString();
168    }
169
170    protected void command(String command) {
171        if (widget != null) {
172            $(widget.getElement()).tooltip(command);
173        }
174    }
175
176    @Override
177    public void setWidget(final Widget widget) {
178        // Validate
179        if (widget == this.widget) {
180            return;
181        }
182
183        // Remove old child
184        if (this.widget != null) {
185            remove(this.widget);
186        }
187
188        // Logical attach, but don't physical attach; done by jquery.
189        this.widget = widget;
190        if (this.widget == null) {
191            return;
192        }
193
194        setAttribute("data-delay", String.valueOf(options.delay));
195
196        if (options.position != null) {
197            setAttribute("data-position", options.position);
198        }
199
200        if (options.tooltip != null) {
201            setAttribute("data-tooltip", options.tooltip);
202        }
203
204        if (this.widget.isAttached()) {
205            reload();
206        } else {
207            // Smart detect the attachment and detachment of widget to update the tooltip
208            widget.addAttachHandler(event -> {
209                if (event.isAttached()) {
210                    // If its attached - reload the tooltip
211                    reload();
212                } else {
213                    // If it was detached - unload the tooltip
214                    unload();
215                }
216            });
217        }
218    }
219
220    @Override
221    public void add(final Widget child) {
222        if (getWidget() != null) {
223            throw new IllegalStateException("Can only contain one child widget");
224        }
225        setWidget(child);
226    }
227
228    @Override
229    public void setWidget(final IsWidget w) {
230        setWidget(w.asWidget());
231    }
232
233    @Override
234    public Widget getWidget() {
235        return widget;
236    }
237
238    @Override
239    public void setId(final String id) {
240        this.id = id;
241        if (widget != null) {
242            widget.getElement().setId(id);
243        }
244    }
245
246    @Override
247    public String getId() {
248        return (widget == null) ? id : widget.getElement().getId();
249    }
250
251    @Override
252    public void setPosition(final Position position) {
253        options.position = position.getCssName();
254
255        setAttribute("data-position", position.getCssName());
256    }
257
258    @Override
259    public Position getPosition() {
260        return options.position != null ? Position.fromStyleName(options.position) : null;
261    }
262
263    public void setDelayMs(final int delayMs) {
264        options.delay = delayMs;
265
266        setAttribute("data-delay", String.valueOf(delayMs));
267    }
268
269    public int getDelayMs() {
270        return options.delay;
271    }
272
273    /**
274     * Gets the tooltip's display string
275     *
276     * @return String tooltip display string
277     */
278    @Override
279    public String getText() {
280        return options.tooltip;
281    }
282
283    /**
284     * Sets the tooltip's display string
285     *
286     * @param text String display string
287     */
288    @Override
289    public void setText(final String text) {
290        options.tooltip = text;
291
292        setAttribute("data-tooltip", text);
293    }
294
295    /**
296     * @deprecated Use {@link #getHtml}
297     */
298    @Deprecated
299    public String getTooltipHTML() {
300        return getHtml();
301    }
302
303    /**
304     * @deprecated Use {@link #setHtml}
305     */
306    @Deprecated
307    public void setTooltipHTML(String html) {
308        setHtml(html);
309    }
310
311    /**
312     * Get the html of the tooltip.
313     */
314    public String getHtml() {
315        return html;
316    }
317
318    /**
319     * Set the html as value inside the tooltip.
320     */
321    public void setHtml(String html) {
322        this.html = html;
323
324        Element element = widget.getElement();
325        if (widget.isAttached()) {
326            $("#" + element.getAttribute("data-tooltip-id"))
327                    .find("span")
328                    .html(html != null ? html : "");
329        } else {
330            widget.addAttachHandler(event ->
331                    $("#" + element.getAttribute("data-tooltip-id"))
332                            .find("span")
333                            .html(html != null ? html : ""));
334        }
335    }
336
337    public void setAttribute(String attr, String value) {
338        if (widget != null) {
339            AttachEvent.Handler handler = event -> {
340                widget.getElement().setAttribute(attr, value);
341            };
342            if (widget.isAttached()) {
343                handler.onAttachOrDetach(null);
344            } else {
345                EventHelper.onAttachOnce(widget, handler);
346            }
347        }
348    }
349}