001/*
002 *  Copyright 2012 GWT-Bootstrap
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of 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,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.github.gwtbootstrap.client.ui;
017
018import com.github.gwtbootstrap.client.ui.base.HasId;
019import com.github.gwtbootstrap.client.ui.base.HasStyle;
020import com.github.gwtbootstrap.client.ui.base.IsResponsive;
021import com.github.gwtbootstrap.client.ui.base.IsSearchQuery;
022import com.github.gwtbootstrap.client.ui.base.ResponsiveHelper;
023import com.github.gwtbootstrap.client.ui.base.SearchQueryStyleHelper;
024import com.github.gwtbootstrap.client.ui.base.Style;
025import com.github.gwtbootstrap.client.ui.base.StyleHelper;
026import com.github.gwtbootstrap.client.ui.constants.Constants;
027import com.github.gwtbootstrap.client.ui.constants.Device;
028import com.google.gwt.dom.client.Document;
029import com.google.gwt.dom.client.InputElement;
030import com.google.gwt.dom.client.LabelElement;
031import com.google.gwt.dom.client.SpanElement;
032import com.google.gwt.editor.client.IsEditor;
033import com.google.gwt.editor.client.LeafValueEditor;
034import com.google.gwt.editor.client.adapters.TakesValueEditor;
035import com.google.gwt.event.dom.client.ClickEvent;
036import com.google.gwt.event.dom.client.ClickHandler;
037import com.google.gwt.event.logical.shared.ValueChangeEvent;
038import com.google.gwt.event.logical.shared.ValueChangeHandler;
039import com.google.gwt.event.shared.HandlerRegistration;
040import com.google.gwt.i18n.client.HasDirection.Direction;
041import com.google.gwt.i18n.shared.DirectionEstimator;
042import com.google.gwt.i18n.shared.HasDirectionEstimator;
043import com.google.gwt.safehtml.shared.SafeHtml;
044import com.google.gwt.user.client.DOM;
045import com.google.gwt.user.client.Element;
046import com.google.gwt.user.client.Event;
047import com.google.gwt.user.client.EventListener;
048import com.google.gwt.user.client.ui.ButtonBase;
049import com.google.gwt.user.client.ui.DirectionalTextHelper;
050import com.google.gwt.user.client.ui.FormPanel;
051import com.google.gwt.user.client.ui.HasDirectionalSafeHtml;
052import com.google.gwt.user.client.ui.HasName;
053import com.google.gwt.user.client.ui.HasValue;
054import com.google.gwt.user.client.ui.HasWordWrap;
055import com.google.gwt.user.client.ui.RadioButton;
056import com.google.gwt.user.client.ui.UIObject;
057import com.google.gwt.user.client.ui.Widget;
058
059import static com.github.gwtbootstrap.client.ui.constants.Constants.CHECKBOX;
060import static com.github.gwtbootstrap.client.ui.constants.Constants.RADIO;
061
062/**
063 * CheckBox widgets.
064 * <p>
065 * Re-design for Bootstrap.
066 * </p>
067 *
068 * <p>CheckBox Usage:</p>
069 * <pre>{@code
070 * <b:CheckBox formValue="remember-me" text="Remember me"/>
071 * }</pre>
072 * <p>Above code renders to...</p>
073 * <pre>{@code
074 *  <label class="checkbox pull-left" for="gwt-uid-n">
075 *      <input id="gwt-uid-n" type="checkbox" value="remember-me" tabindex="0"></input>
076 *      <span>Remember me</span>
077 *  </label>}</pre>
078 *  ...where n is any number generated by GWT.
079 * @since 2.0.4.0
080 * @author ohashi keisuke
081 */
082public class CheckBox extends ButtonBase implements HasName, HasValue<Boolean>, HasWordWrap, HasDirectionalSafeHtml, HasDirectionEstimator, IsEditor<LeafValueEditor<Boolean>>, IsSearchQuery, HasId, IsResponsive, HasStyle{
083
084        public static final DirectionEstimator DEFAULT_DIRECTION_ESTIMATOR = DirectionalTextHelper.DEFAULT_DIRECTION_ESTIMATOR;
085
086        final DirectionalTextHelper directionalTextHelper;
087
088        InputElement inputElem;
089        SpanElement spanElem;
090        
091
092        private LeafValueEditor<Boolean> editor;
093
094        private boolean valueChangeHandlerInitialized;
095
096        /**
097         * Creates a check box with no label.
098         */
099        public CheckBox() {
100                this(DOM.createInputCheck());
101        }
102
103        /**
104         * Creates a check box with the specified text label.
105         * 
106         * @param label
107         *            the check box's label
108         */
109        public CheckBox(SafeHtml label) {
110                this(label.asString(), true);
111        }
112
113        /**
114         * Creates a check box with the specified text label.
115         * 
116         * @param label
117         *            the check box's label
118         * @param dir
119         *            the text's direction. Note that {@code DEFAULT} means
120         *            direction should be inherited from the widget's parent
121         *            element.
122         */
123        public CheckBox(SafeHtml label,
124                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
133         *            the check box's label
134         * @param directionEstimator
135         *            A DirectionEstimator object used for automatic direction
136         *            adjustment. For convenience,
137         *            {@link #DEFAULT_DIRECTION_ESTIMATOR} can be used.
138         */
139        public CheckBox(SafeHtml label,
140                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
150         *            the check box's label
151         */
152        public CheckBox(String label) {
153                this();
154                setText(label);
155        }
156
157        /**
158         * Creates a check box with the specified text label.
159         * 
160         * @param label
161         *            the check box's label
162         * @param dir
163         *            the text's direction. Note that {@code DEFAULT} means
164         *            direction should be inherited from the widget's parent
165         *            element.
166         */
167        public CheckBox(String label,
168                Direction dir) {
169                this();
170                setText(label, dir);
171        }
172
173        /**
174         * Creates a label with the specified text and a default direction
175         * estimator.
176         * 
177         * @param label
178         *            the check box's label
179         * @param directionEstimator
180         *            A DirectionEstimator object used for automatic direction
181         *            adjustment. For convenience,
182         *            {@link #DEFAULT_DIRECTION_ESTIMATOR} can be used.
183         */
184        public CheckBox(String label,
185                DirectionEstimator directionEstimator) {
186                this();
187                setDirectionEstimator(directionEstimator);
188                setText(label);
189        }
190
191        /**
192         * Creates a check box with the specified text label.
193         * 
194         * @param label
195         *            the check box's label
196         * @param asHTML
197         *            <code>true</code> to treat the specified label as html
198         */
199        public CheckBox(String label,
200                boolean asHTML) {
201                this();
202                if (asHTML) {
203                        setHTML(label);
204                } else {
205                        setText(label);
206                }
207        }
208
209        protected CheckBox(Element elem) {
210                super(DOM.createLabel());
211                
212                assert elem.hasAttribute("type") : "The elem should has type attributes";
213
214        String elementType = elem.getAttribute("type");
215        if(CHECKBOX.equalsIgnoreCase(elementType)) {
216                        this.setStyleName(CHECKBOX);
217                } else if(RADIO.equalsIgnoreCase(elementType)){
218                        this.setStyleName(RADIO);
219                }
220
221                inputElem = InputElement.as(elem);
222                spanElem = Document.get().createSpanElement();
223                getElement().appendChild(inputElem);
224                getElement().appendChild(spanElem);
225
226                String uid = DOM.createUniqueId();
227                inputElem.setPropertyString("id", uid);
228                asLabel().setHtmlFor(uid);
229                directionalTextHelper = new DirectionalTextHelper(spanElem, false);
230                setTabIndex(0);
231        }
232
233        public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Boolean> handler) {
234                // Is this the first value change handler? If so, time to add handlers
235                if (!valueChangeHandlerInitialized) {
236                        ensureDomEventHandlers();
237                        valueChangeHandlerInitialized = true;
238                }
239                return addHandler(handler, ValueChangeEvent.getType());
240        }
241
242        public LeafValueEditor<Boolean> asEditor() {
243                if (editor == null) {
244                        editor = TakesValueEditor.of(this);
245                }
246                return editor;
247        }
248
249        public DirectionEstimator getDirectionEstimator() {
250                return directionalTextHelper.getDirectionEstimator();
251        }
252
253        /**
254         * Returns the value property of the input element that backs this widget.
255         * This is the value that will be associated with the CheckBox name and
256         * submitted to the server if a {@link FormPanel} that holds it is submitted
257         * and the box is checked.
258         * <p>
259         * Don't confuse this with {@link #getValue}, which returns true or false if
260         * the widget is checked.
261         */
262        public String getFormValue() {
263                return inputElem.getValue();
264        }
265
266        @Override
267        public String getHTML() {
268                return directionalTextHelper.getTextOrHtml(true);
269        }
270
271        public String getName() {
272                return inputElem.getName();
273        }
274
275        @Override
276        public int getTabIndex() {
277                return inputElem.getTabIndex();
278        }
279
280        @Override
281        public String getText() {
282                return directionalTextHelper.getTextOrHtml(false);
283        }
284
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
293         * checkbox input element wrapped by this widget. For access to that
294         * property, see {@link #getFormValue()}
295         * 
296         * @return <code>true</code> if the check box is checked, false otherwise.
297         *         Will not return null
298         */
299        public Boolean getValue() {
300                if (isAttached()) {
301                        return inputElem.isChecked();
302                } else {
303                        return inputElem.isDefaultChecked();
304                }
305        }
306
307        public boolean getWordWrap() {
308                return !getElement().getStyle().getProperty("whiteSpace").equals("nowrap");
309        }
310
311        /**
312         * Determines whether this check box is currently checked.
313         * 
314         * @return <code>true</code> if the check box is checked
315         * @deprecated Use {@link #getValue} instead
316         */
317        @Deprecated
318        public boolean isChecked() {
319                // Funny comparison b/c getValue could in theory return null
320                return getValue() == true;
321        }
322
323        @Override
324        public boolean isEnabled() {
325                return !inputElem.isDisabled();
326        }
327
328        @Override
329        public void setAccessKey(char key) {
330                inputElem.setAccessKey("" + key);
331        }
332
333        /**
334         * Checks or unchecks this check box. Does not fire {@link ValueChangeEvent}
335         * . (If you want the event to fire, use {@link #setValue(Boolean, boolean)}
336         * )
337         * 
338         * @param checked
339         *            <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        public void setDirectionEstimator(boolean enabled) {
353                directionalTextHelper.setDirectionEstimator(enabled);
354        }
355
356        /**
357         * {@inheritDoc}
358         * <p>
359         * Note: DirectionEstimator should be set before the label has any content;
360         * it's highly recommended to set it using a constructor. Reason: if the
361         * label already has non-empty content, this will update its direction
362         * according to the new estimator's result. This may cause flicker, and thus
363         * should be avoided.
364         */
365        public void setDirectionEstimator(DirectionEstimator directionEstimator) {
366                directionalTextHelper.setDirectionEstimator(directionEstimator);
367        }
368
369        @Override
370        public void setEnabled(boolean enabled) {
371                inputElem.setDisabled(!enabled);
372                if (enabled) {
373                        inputElem.removeClassName(Constants.DISABLED);
374                } else {
375                        inputElem.addClassName(Constants.DISABLED);
376                }
377        }
378
379        @Override
380        public void setFocus(boolean focused) {
381                if (focused) {
382                        inputElem.focus();
383                } else {
384                        inputElem.blur();
385                }
386        }
387
388        /**
389         * Set the value property on the input element that backs this widget. This
390         * is the value that will be associated with the CheckBox's name and
391         * submitted to the server if a {@link FormPanel} that holds it is submitted
392         * and the box is checked.
393         * <p>
394         * Don't confuse this with {@link #setValue}, which actually checks and
395         * unchecks the box.
396         * 
397         * @param value
398         */
399        public void setFormValue(String value) {
400                inputElem.setAttribute("value", value);
401        }
402
403        public void setHTML(SafeHtml html, Direction dir) {
404                directionalTextHelper.setTextOrHtml(html.asString(), dir, true);
405        }
406
407        @Override
408        public void setHTML(String html) {
409                directionalTextHelper.setTextOrHtml(html, true);
410        }
411
412        public void setName(String name) {
413                inputElem.setName(name);
414        }
415
416        @Override
417        public void setTabIndex(int index) {
418                // Need to guard against call to setTabIndex before inputElem is
419                // initialized. This happens because FocusWidget's (a superclass of
420                // CheckBox) setElement method calls setTabIndex before inputElem is
421                // initialized. See CheckBox's protected constructor for more
422                // information.
423                if (inputElem != null) {
424                        inputElem.setTabIndex(index);
425                }
426        }
427
428        @Override
429        public void setText(String text) {
430                directionalTextHelper.setTextOrHtml(text, false);
431        }
432
433        public void setText(String text, Direction dir) {
434                directionalTextHelper.setTextOrHtml(text, dir, false);
435        }
436
437        /**
438         * Checks or unchecks the check box.
439         * <p>
440         * Note that this <em>does not</em> set the value property of the checkbox
441         * input element wrapped by this widget. For access to that property, see
442         * {@link #setFormValue(String)}
443         * 
444         * @param value
445         *            true to check, false to uncheck; null value implies false
446         */
447        public void setValue(Boolean value) {
448                setValue(value, false);
449        }
450
451        /**
452         * Checks or unchecks the check box, firing {@link ValueChangeEvent} if
453         * appropriate.
454         * <p>
455         * Note that this <em>does not</em> set the value property of the checkbox
456         * input element wrapped by this widget. For access to that property, see
457         * {@link #setFormValue(String)}
458         * 
459         * @param value
460         *            true to check, false to uncheck; null value implies false
461         * @param fireEvents
462         *            If true, and value has changed, fire a
463         *            {@link ValueChangeEvent}
464         */
465        public void setValue(Boolean value, boolean fireEvents) {
466                if (value == null) {
467                        value = Boolean.FALSE;
468                }
469
470                Boolean oldValue = getValue();
471                inputElem.setChecked(value);
472                inputElem.setDefaultChecked(value);
473                if (value.equals(oldValue)) {
474                        return;
475                }
476                if (fireEvents) {
477                        ValueChangeEvent.fire(this, value);
478                }
479        }
480
481        public void setWordWrap(boolean wrap) {
482                getElement().getStyle().setProperty("whiteSpace", wrap
483                                                                                                                                ? "normal"
484                                                                                                                                : "nowrap");
485        }
486
487        // Unlike other widgets the CheckBox sinks on its inputElement, not
488        // its wrapper
489        @Override
490        public void sinkEvents(int eventBitsToAdd) {
491                if (isOrWasAttached()) {
492                        Event.sinkEvents(inputElem, eventBitsToAdd | Event.getEventsSunk(inputElem));
493                } else {
494                        super.sinkEvents(eventBitsToAdd);
495                }
496        }
497
498        protected void ensureDomEventHandlers() {
499                addClickHandler(new ClickHandler() {
500
501                        public void onClick(ClickEvent event) {
502                                // Checkboxes always toggle their value, no need to compare
503                                // with old value. Radio buttons are not so lucky, see
504                                // overrides in RadioButton
505                                ValueChangeEvent.fire(CheckBox.this, getValue());
506                        }
507                });
508        }
509
510        /**
511         * <b>Affected Elements:</b>
512         * <ul>
513         * <li>-label = label next to checkbox.</li>
514         * </ul>
515         * 
516         * @see UIObject#onEnsureDebugId(String)
517         */
518        @Override
519        protected void onEnsureDebugId(String baseID) {
520                super.onEnsureDebugId(baseID);
521                ensureDebugId(asLabel(), baseID, "label");
522                ensureDebugId(inputElem, baseID, "input");
523                asLabel().setHtmlFor(inputElem.getId());
524        }
525
526        /**
527         * This method is called when a widget is attached to the browser's
528         * document. onAttach needs special handling for the CheckBox case. Must
529         * still call {@link Widget#onAttach()} to preserve the
530         * <code>onAttach</code> contract.
531         */
532        @Override
533        protected void onLoad() {
534                setEventListener(inputElem, this);
535        }
536
537        /**
538         * This method is called when a widget is detached from the browser's
539         * document. Overridden because of IE bug that throws away checked state and
540         * in order to clear the event listener off of the <code>inputElem</code>.
541         */
542        @Override
543        protected void onUnload() {
544                // Clear out the inputElem's event listener (breaking the circular
545                // reference between it and the widget).
546                setEventListener(asOld(inputElem), null);
547                setValue(getValue());
548        }
549
550        /**
551         * Replace the current input element with a new one. Preserves all state
552         * except for the name property, for nasty reasons related to radio button
553         * grouping. (See implementation of {@link RadioButton#setName}.)
554         * 
555         * @param elem
556         *            the new input element
557         */
558        protected void replaceInputElement(Element elem) {
559                InputElement newInputElem = InputElement.as(elem);
560                // Collect information we need to set
561                int tabIndex = getTabIndex();
562                boolean checked = getValue();
563                boolean enabled = isEnabled();
564                String formValue = getFormValue();
565                String uid = inputElem.getId();
566                String accessKey = inputElem.getAccessKey();
567                int sunkEvents = Event.getEventsSunk(inputElem);
568
569                // Clear out the old input element
570                setEventListener(asOld(inputElem), null);
571
572                getElement().replaceChild(newInputElem, inputElem);
573
574                // Sink events on the new element
575                Event.sinkEvents(elem, Event.getEventsSunk(inputElem));
576                Event.sinkEvents(inputElem, 0);
577                inputElem = newInputElem;
578
579                // Setup the new element
580                Event.sinkEvents(inputElem, sunkEvents);
581                inputElem.setId(uid);
582                if (!"".equals(accessKey)) {
583                        inputElem.setAccessKey(accessKey);
584                }
585                setTabIndex(tabIndex);
586                setValue(checked);
587                setEnabled(enabled);
588                setFormValue(formValue);
589
590                // Set the event listener
591                if (isAttached()) {
592                        setEventListener(asOld(inputElem), this);
593                }
594        }
595
596        private Element asOld(com.google.gwt.dom.client.Element elem) {
597                Element oldSchool = elem.cast();
598                return oldSchool;
599        }
600
601        private void setEventListener(com.google.gwt.dom.client.Element e, EventListener listener) {
602                DOM.setEventListener(asOld(e), listener);
603        }
604
605        protected LabelElement asLabel() {
606                return LabelElement.as(getElement());
607        }
608
609        public void setInline(boolean inline) {
610                if(getStyleName().contains(Constants.INLINE)) {
611                        removeStyleName(Constants.INLINE);
612                }
613                
614                if(inline) {
615                        addStyleName(Constants.INLINE);
616                }
617        }
618
619        /**
620         * {@inheritDoc}
621         */
622        @Override
623        public void setSearchQuery(boolean searchQuery) {
624                SearchQueryStyleHelper.setSearchQuery(inputElem, searchQuery);
625        }
626
627        /**
628         * {@inheritDoc}
629         */
630        @Override
631        public boolean isSearchQuery() {
632                return SearchQueryStyleHelper.isSearchQuery(inputElem);
633        }
634
635        /**
636         * {@inheritDoc}
637         */
638        @Override
639        public String getId() {
640                return inputElem.getId();
641        }
642
643        /**
644         * {@inheritDoc}
645         */
646        @Override
647        public void setId(String id) {
648                inputElem.setId(id);
649                asLabel().setHtmlFor(id);
650        }
651
652
653        /**
654         * {@inheritDoc}
655         */
656        @Override
657        public void setShowOn(Device device) {
658                ResponsiveHelper.setShowOn(this, device);
659        }
660
661        /**
662         * {@inheritDoc}
663         */
664        @Override
665        public void setHideOn(Device device) {
666                ResponsiveHelper.setHideOn(this, device);
667                
668        }
669
670        /**
671         * {@inheritDoc}
672         */
673        @Override
674        public void setStyle(Style style) {
675                StyleHelper.setStyle(this, style);
676        }
677
678        /**
679         * {@inheritDoc}
680         */
681        @Override
682        public void addStyle(Style style) {
683                StyleHelper.addStyle(this, style);
684        }
685
686        /**
687         * {@inheritDoc}
688         */
689        @Override
690        public void removeStyle(Style style) {
691                StyleHelper.removeStyle(this, style);
692                
693        }
694}