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