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