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 */
020/*
021 * Copyright 2009 Google Inc.
022 * 
023 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
024 * use this file except in compliance with the License. You may obtain a copy of
025 * the License at
026 * 
027 * http://www.apache.org/licenses/LICENSE-2.0
028 * 
029 * Unless required by applicable law or agreed to in writing, software
030 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
031 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
032 * License for the specific language governing permissions and limitations under
033 * the License.
034 */
035package gwt.material.design.client.base;
036
037import com.google.gwt.dom.client.Document;
038import com.google.gwt.dom.client.Element;
039import com.google.gwt.dom.client.InputElement;
040import com.google.gwt.dom.client.LabelElement;
041import com.google.gwt.dom.client.Style.WhiteSpace;
042import com.google.gwt.editor.client.IsEditor;
043import com.google.gwt.editor.client.LeafValueEditor;
044import com.google.gwt.editor.client.adapters.TakesValueEditor;
045import com.google.gwt.event.logical.shared.ValueChangeEvent;
046import com.google.gwt.event.logical.shared.ValueChangeHandler;
047import com.google.gwt.event.shared.HandlerRegistration;
048import com.google.gwt.i18n.client.HasDirection.Direction;
049import com.google.gwt.i18n.shared.DirectionEstimator;
050import com.google.gwt.i18n.shared.HasDirectionEstimator;
051import com.google.gwt.safehtml.shared.SafeHtml;
052import com.google.gwt.user.client.DOM;
053import com.google.gwt.user.client.Event;
054import com.google.gwt.user.client.ui.*;
055import gwt.material.design.client.constants.CssName;
056
057/**
058 * A standard check box widget.
059 * <p>
060 * This class also serves as a base class for
061 * {@link RadioButton}.
062 * <p>
063 * <p>
064 * <img class='gallery' src='doc-files/CheckBox.png'/>
065 * </p>
066 * <p>
067 * <p>
068 * <h3>Built-in Bidi Text Support</h3>
069 * This widget is capable of automatically adjusting its direction according to
070 * its content. This feature is controlled by {@link #setDirectionEstimator} or
071 * passing a DirectionEstimator parameter to the constructor, and is off by
072 * default.
073 * </p>
074 * <p>
075 * <h3>CSS Style Rules</h3>
076 * <dl>
077 * <dt>.gwt-CheckBox</dt>
078 * <dd>the outer element</dd>
079 * <dt>.gwt-CheckBox-disabled</dt>
080 * <dd>applied when Checkbox is disabled</dd>
081 * </dl>
082 * <p>
083 * <p>
084 * <h3>Example</h3>
085 * { @example com.google.gwt.examples.CheckBoxExample}
086 * </p>
087 */
088public class BaseCheckBox extends ButtonBase implements HasName, HasValue<Boolean>,
089        HasWordWrap, HasDirectionalSafeHtml, HasDirectionEstimator, IsEditor<LeafValueEditor<Boolean>> {
090
091    public static final DirectionEstimator DEFAULT_DIRECTION_ESTIMATOR =
092            DirectionalTextHelper.DEFAULT_DIRECTION_ESTIMATOR;
093
094    final DirectionalTextHelper directionalTextHelper;
095    InputElement inputElem;
096    LabelElement labelElem;
097    private LeafValueEditor<Boolean> editor;
098    private boolean valueChangeHandlerInitialized;
099
100    /**
101     * Creates a check box with no label.
102     */
103    public BaseCheckBox() {
104        this(DOM.createSpan());
105        setStyleName(CssName.GWT_CHECKBOX);
106    }
107
108    /**
109     * Creates a check box with the specified text label.
110     *
111     * @param label the check box's label
112     */
113    public BaseCheckBox(SafeHtml label) {
114        this(label.asString(), true);
115    }
116
117    /**
118     * Creates a check box with the specified text label.
119     *
120     * @param label the check box's label
121     * @param dir   the text's direction. Note that {@code DEFAULT} means direction
122     *              should be inherited from the widget's parent element.
123     */
124    public BaseCheckBox(SafeHtml label, Direction dir) {
125        this();
126        setHTML(label, dir);
127    }
128
129    /**
130     * Creates a check box with the specified text label.
131     *
132     * @param label              the check box's label
133     * @param directionEstimator A DirectionEstimator object used for automatic
134     *                           direction adjustment. For convenience,
135     *                           {@link #DEFAULT_DIRECTION_ESTIMATOR} can be used.
136     */
137    public BaseCheckBox(SafeHtml label, DirectionEstimator directionEstimator) {
138        this();
139        setDirectionEstimator(directionEstimator);
140        setHTML(label.asString());
141    }
142
143    /**
144     * Creates a check box with the specified text label.
145     *
146     * @param label the check box's label
147     */
148    public BaseCheckBox(String label) {
149        this();
150        setText(label);
151    }
152
153    /**
154     * Creates a check box with the specified text label.
155     *
156     * @param label the check box's label
157     * @param dir   the text's direction. Note that {@code DEFAULT} means direction
158     *              should be inherited from the widget's parent element.
159     */
160    public BaseCheckBox(String label, Direction dir) {
161        this();
162        setText(label, dir);
163    }
164
165    /**
166     * Creates a label with the specified text and a default direction estimator.
167     *
168     * @param label              the check box's label
169     * @param directionEstimator A DirectionEstimator object used for automatic
170     *                           direction adjustment. For convenience,
171     *                           {@link #DEFAULT_DIRECTION_ESTIMATOR} can be used.
172     */
173    public BaseCheckBox(String label, DirectionEstimator directionEstimator) {
174        this();
175        setDirectionEstimator(directionEstimator);
176        setText(label);
177    }
178
179    /**
180     * Creates a check box with the specified text label.
181     *
182     * @param label  the check box's label
183     * @param asHTML <code>true</code> to treat the specified label as html
184     */
185    public BaseCheckBox(String label, boolean asHTML) {
186        this();
187        if (asHTML) {
188            setHTML(label);
189        } else {
190            setText(label);
191        }
192    }
193
194    protected BaseCheckBox(Element elem) {
195        super(elem);
196
197        inputElem = InputElement.as(DOM.createInputCheck());
198        labelElem = Document.get().createLabelElement();
199
200        getElement().appendChild(inputElem);
201        getElement().appendChild(labelElem);
202
203        String uid = DOM.createUniqueId();
204        inputElem.setPropertyString("id", uid);
205        labelElem.setHtmlFor(uid);
206
207        directionalTextHelper = new DirectionalTextHelper(labelElem, true);
208
209        // Accessibility: setting tab index to be 0 by default, ensuring element
210        // appears in tab sequence. FocusWidget's setElement method already
211        // calls setTabIndex, which is overridden below. However, at the time
212        // that this call is made, inputElem has not been created. So, we have
213        // to call setTabIndex again, once inputElem has been created.
214        setTabIndex(0);
215    }
216
217    @Override
218    public HandlerRegistration addValueChangeHandler(
219            final ValueChangeHandler<Boolean> handler) {
220        // Is this the first value change handler? If so, time to add handlers
221        if (!valueChangeHandlerInitialized) {
222            ensureDomEventHandlers();
223            valueChangeHandlerInitialized = true;
224        }
225        return addHandler(handler, ValueChangeEvent.getType());
226    }
227
228    @Override
229    public LeafValueEditor<Boolean> asEditor() {
230        if (editor == null) {
231            editor = TakesValueEditor.of(this);
232        }
233        return editor;
234    }
235
236    @Override
237    public DirectionEstimator getDirectionEstimator() {
238        return directionalTextHelper.getDirectionEstimator();
239    }
240
241    /**
242     * Returns the value property of the input element that backs this widget.
243     * This is the value that will be associated with the CheckBox name and
244     * submitted to the server if a {@link FormPanel} that holds it is submitted
245     * and the box is checked.
246     * <p>
247     * Don't confuse this with {@link #getValue}, which returns true or false if
248     * the widget is checked.
249     */
250    public String getFormValue() {
251        return inputElem.getValue();
252    }
253
254    @Override
255    public String getHTML() {
256        return directionalTextHelper.getTextOrHtml(true);
257    }
258
259    @Override
260    public String getName() {
261        return inputElem.getName();
262    }
263
264    @Override
265    public int getTabIndex() {
266        return inputElem.getTabIndex();
267    }
268
269    @Override
270    public String getText() {
271        return directionalTextHelper.getTextOrHtml(false);
272    }
273
274    @Override
275    public Direction getTextDirection() {
276        return directionalTextHelper.getTextDirection();
277    }
278
279    /**
280     * Determines whether this check box is currently checked.
281     * <p>
282     * Note that this <em>does not</em> return the value property of the checkbox
283     * input element wrapped by this widget. For access to that property, see
284     * {@link #getFormValue()}
285     *
286     * @return <code>true</code> if the check box is checked, false otherwise.
287     * Will not return null
288     */
289    @Override
290    public Boolean getValue() {
291        if (isAttached()) {
292            return inputElem.isChecked();
293        } else {
294            return inputElem.isDefaultChecked();
295        }
296    }
297
298    @Override
299    public boolean getWordWrap() {
300        return !WhiteSpace.NOWRAP.getCssName().equals(getElement().getStyle().getWhiteSpace());
301    }
302
303    /**
304     * Determines whether this check box is currently checked.
305     *
306     * @return <code>true</code> if the check box is checked
307     * @deprecated Use {@link #getValue} instead
308     */
309    @Deprecated
310    public boolean isChecked() {
311        // Funny comparison b/c getValue could in theory return null
312        return getValue() == true;
313    }
314
315    @Override
316    public boolean isEnabled() {
317        return !inputElem.isDisabled();
318    }
319
320    @Override
321    public void setAccessKey(char key) {
322        inputElem.setAccessKey("" + key);
323    }
324
325    /**
326     * {@inheritDoc}
327     * <p>
328     * See note at {@link #setDirectionEstimator(DirectionEstimator)}.
329     */
330    @Override
331    public void setDirectionEstimator(boolean enabled) {
332        directionalTextHelper.setDirectionEstimator(enabled);
333    }
334
335    /**
336     * {@inheritDoc}
337     * <p>
338     * Note: DirectionEstimator should be set before the label has any content;
339     * it's highly recommended to set it using a constructor. Reason: if the
340     * label already has non-empty content, this will update its direction
341     * according to the new estimator's result. This may cause flicker, and thus
342     * should be avoided.
343     */
344    @Override
345    public void setDirectionEstimator(DirectionEstimator directionEstimator) {
346        directionalTextHelper.setDirectionEstimator(directionEstimator);
347    }
348
349    @Override
350    public void setEnabled(boolean enabled) {
351        inputElem.setDisabled(!enabled);
352        if (enabled) {
353            removeStyleDependentName(CssName.DISABLED);
354        } else {
355            addStyleDependentName(CssName.DISABLED);
356        }
357    }
358
359    @Override
360    public void setFocus(boolean focused) {
361        if (focused) {
362            inputElem.focus();
363        } else {
364            inputElem.blur();
365        }
366    }
367
368    /**
369     * Set the value property on the input element that backs this widget. This is
370     * the value that will be associated with the CheckBox's name and submitted to
371     * the server if a {@link FormPanel} that holds it is submitted and the box is
372     * checked.
373     * <p>
374     * Don't confuse this with {@link #setValue}, which actually checks and
375     * unchecks the box.
376     *
377     * @param value
378     */
379    public void setFormValue(String value) {
380        inputElem.setAttribute("value", value);
381    }
382
383    @Override
384    public void setHTML(SafeHtml html, Direction dir) {
385        directionalTextHelper.setTextOrHtml(html.asString(), dir, true);
386    }
387
388    @Override
389    public void setHTML(String html) {
390        directionalTextHelper.setTextOrHtml(html, true);
391    }
392
393    @Override
394    public void setName(String name) {
395        inputElem.setName(name);
396    }
397
398    @Override
399    public void setTabIndex(int index) {
400        // Need to guard against call to setTabIndex before inputElem is
401        // initialized. This happens because FocusWidget's (a superclass of
402        // CheckBox) setElement method calls setTabIndex before inputElem is
403        // initialized. See CheckBox's protected constructor for more information.
404        if (inputElem != null) {
405            inputElem.setTabIndex(index);
406        }
407    }
408
409    @Override
410    public void setText(String text) {
411        directionalTextHelper.setTextOrHtml(text, false);
412    }
413
414    @Override
415    public void setText(String text, Direction dir) {
416        directionalTextHelper.setTextOrHtml(text, dir, false);
417    }
418
419    /**
420     * Checks or unchecks the check box.
421     * <p>
422     * Note that this <em>does not</em> set the value property of the checkbox
423     * input element wrapped by this widget. For access to that property, see
424     * {@link #setFormValue(String)}
425     *
426     * @param value true to check, false to uncheck; null value implies false
427     */
428    @Override
429    public void setValue(Boolean value) {
430        setValue(value, false);
431    }
432
433    /**
434     * Checks or unchecks the check box, firing {@link ValueChangeEvent} if
435     * appropriate.
436     * <p>
437     * Note that this <em>does not</em> set the value property of the checkbox
438     * input element wrapped by this widget. For access to that property, see
439     * {@link #setFormValue(String)}
440     *
441     * @param value      true to check, false to uncheck; null value implies false
442     * @param fireEvents If true, and value has changed, fire a
443     *                   {@link ValueChangeEvent}
444     */
445    @Override
446    public void setValue(Boolean value, boolean fireEvents) {
447        if (value == null) {
448            value = Boolean.FALSE;
449        }
450
451        Boolean oldValue = getValue();
452        inputElem.setChecked(value);
453        inputElem.setDefaultChecked(value);
454        if (value.equals(oldValue)) {
455            return;
456        }
457        if (fireEvents) {
458            ValueChangeEvent.fire(this, value);
459        }
460    }
461
462    @Override
463    public void setWordWrap(boolean wrap) {
464        getElement().getStyle().setWhiteSpace(wrap ? WhiteSpace.NORMAL : WhiteSpace.NOWRAP);
465    }
466
467    // Unlike other widgets the CheckBox sinks on its inputElement, not
468    // its wrapper
469    @Override
470    public void sinkEvents(int eventBitsToAdd) {
471        if (isOrWasAttached()) {
472            Event.sinkEvents(inputElem, eventBitsToAdd
473                    | Event.getEventsSunk(inputElem));
474        } else {
475            super.sinkEvents(eventBitsToAdd);
476        }
477    }
478
479    protected void ensureDomEventHandlers() {
480        addClickHandler(event -> {
481            // Checkboxes always toggle their value, no need to compare
482            // with old value. Radio buttons are not so lucky, see
483            // overrides in RadioButton
484            ValueChangeEvent.fire(BaseCheckBox.this, getValue());
485        });
486    }
487
488    /**
489     * <b>Affected Elements:</b>
490     * <ul>
491     * <li>-label = label next to checkbox.</li>
492     * </ul>
493     *
494     * @see UIObject#onEnsureDebugId(String)
495     */
496    @Override
497    protected void onEnsureDebugId(String baseID) {
498        super.onEnsureDebugId(baseID);
499        ensureDebugId(labelElem, baseID, "label");
500        ensureDebugId(inputElem, baseID, "input");
501        labelElem.setHtmlFor(inputElem.getId());
502    }
503
504    /**
505     * This method is called when a widget is attached to the browser's document.
506     * onAttach needs special handling for the CheckBox case. Must still call
507     * {@link Widget#onAttach()} to preserve the <code>onAttach</code> contract.
508     */
509    @Override
510    protected void onLoad() {
511        DOM.setEventListener(inputElem, this);
512    }
513
514    /**
515     * This method is called when a widget is detached from the browser's
516     * document. Overridden because of IE bug that throws away checked state and
517     * in order to clear the event listener off of the <code>inputElem</code>.
518     */
519    @Override
520    protected void onUnload() {
521        // Clear out the inputElem's event listener (breaking the circular
522        // reference between it and the widget).
523        DOM.setEventListener(inputElem, null);
524        setValue(getValue());
525    }
526}