001 
002 /*
003  *  JScripter Standard 1.0 - To Script In Java
004  *  Copyright (C) 2008-2011  J.J.Liu<jianjunliu@126.com> <http://www.jscripter.org>
005  *  
006  *  This program is free software: you can redistribute it and/or modify
007  *  it under the terms of the GNU Affero General Public License as published by
008  *  the Free Software Foundation, either version 3 of the License, or
009  *  (at your option) any later version.
010  *  
011  *  This program is distributed in the hope that it will be useful,
012  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014  *  GNU Affero General Public License for more details.
015  *  
016  *  You should have received a copy of the GNU Affero General Public License
017  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
018  */
019 
020 package jsx.ui.ctrl;
021 
022 import js.ArrayLike;
023 import js.Id;
024 import js.Js;
025 import js.Vars;
026 import jsx.core.ArrayLikes;
027 import jsx.core.ObjectLikes;
028 import jsx.dom.Markups;
029 import jsx.ui.Component;
030 import jsx.ui.event.Change;
031 import jsx.ui.event.Check;
032 import jsx.ui.event.Click;
033 import jsx.ui.event.OnChange;
034 import jsx.ui.event.Uncheck;
035 import jsx.ui.event.OnCheck;
036 import jsx.ui.event.OnClick;
037 import jsx.ui.event.OnUncheck;
038 
039 /**
040  * <p>A base class for checkable widgets resembling HTML radio buttons.</p>
041  * <p>A checkable widget is a {@link jsx.Source} that fires high level events of {@link Check}, 
042  * {@link Uncheck} and {@link Change}. It is also a high level event listener that listens to 
043  * events of {@link Click}, {@link Check}, {@link Uncheck} and {@link Change}.</p>
044  * 
045  * @author <a href="mailto:jianjunliu@126.com">J.J.Liu (Jianjun Liu)</a> at <a href="http://www.jscripter.org" target="_blank">http://www.jscripter.org</a>
046  */
047 public class Checkable extends Clickable implements OnClick, OnCheck, OnUncheck, OnChange
048 {
049     /**
050      * <p>Constructs a checkable widget.</p>
051      * <p>This constructor invokes the typical constructor {@link #Checkable(Component)}.</p>
052      * @param label The label of HTML text for the checkable widget being constructed.
053      * @since 1.0
054      */
055     public Checkable(String label) {
056         this(new Component(Markups.span(label)));
057     }
058 
059     /**
060      * <p>Typically constructs a checkable widget.</p>
061      * <p>This constructor makes the widget listen to events of {@link Click}, {@link Check}, 
062      * {@link Uncheck} and {@link Change}.</p>
063      * @param e The underlying component for the widget.
064      * @since 1.0
065      */
066     protected Checkable(Component e) {
067         super(e);
068         sub(Clickable.get(e), this, CHECKABLE);
069         addListener(Click.class, this);
070         addListener(Check.class, this);
071         addListener(Uncheck.class, this);
072         addListener(Change.class, this);
073     }
074 
075     private final static Id<Checkable> CHECKABLE = new Id<Checkable>();
076 
077     /**
078      * <p>Gets the {@link Checkable} widget associated with a component.</p>
079      * <p>If the specified component does not have an associated {@link Checkable} widget,
080      * this method will creates one based on that component and associate them.</p>
081      * @param e A component that may have a {@link Checkable} widget based on it.
082      * @return The {@link Checkable} widget associated with <tt>e</tt>.
083      * @since 1.0
084      */
085     public static Checkable get(Component e) {
086         Checkable c = ini(Clickable.get(e)).var(CHECKABLE);
087         return Js.be(c) ? c : new Checkable(e);
088     }
089 
090     private final static Id<Boolean> CHECKED = new Id<Boolean>();
091 
092     /**
093      * <p>Returns the checked state of the checkable widget.</p>
094      * @return <tt>true</tt> if the widget is checked; <tt>false</tt>, otherwise.
095      * @since 1.0
096      */
097     public final boolean checked() {
098         return Js.be(ini(unwrap()).var(CHECKED));
099     }
100 
101     private static final void link(Checkable c1, Checkable c2) {
102         Component.addClasses(c2.unwrap(), c2.subs("grouped"));
103         c1.addListener(Change.class, c2);
104         c2.addListener(Change.class, c1);
105     }
106 
107     private final ArrayLike<Checkable> links() {
108         ArrayLike<Checkable> ret = new Vars<Checkable>().var();
109         ArrayLike<OnChange> ons = getListeners(this, Change.class);
110         for (int i = 0, len = ArrayLikes.length(ons); i < len; i++) {
111             OnChange on = ons.get(i);
112             if (on instanceof Checkable && on != this) {
113                 ArrayLikes.push(ret, (Checkable)on);
114             }
115         }
116         return ret;
117     }
118 
119     /**
120      * <p>Groups with another given checkable widget.</p>
121      * <p>This method expands sub CSS selector "grouped" to the components.</p>
122      * <p>A checkable group allows only one checkable widget to be in checked state at the
123      * same time. Checking a widget in a group will uncheck all the others in the group.</p>
124      * @since 1.0
125      */
126     public final void group(Checkable c) {
127         Component.addClasses(unwrap(), subs("grouped"));
128         link(this, c);
129         ArrayLike<Checkable> links = links();
130         for (int i = 0, len = ArrayLikes.length(links); i < len; i++) {
131             Checkable link = links.get(i);
132             link(this, link);
133         }
134     }
135 
136     /**
137      * <p>Removes the checkable widget from its group.</p>
138      * <p>This method removes sub CSS selector "grouped" from the underlying component.</p>
139      * <p>A checkable group allows only one checkable widget to be in checked state at the
140      * same time. Checking a widget in a group will uncheck all the others in the group.</p>
141      * @since 1.0
142      */
143     public final void ungroup() {
144         Component.removeClasses(unwrap(), subs("grouped"));
145         ArrayLike<Checkable> links = links();
146         for (int i = 0, len = ArrayLikes.length(links); i < len; i++) {
147             Checkable c = links.get(i);
148             removeListener(Change.class, c);
149             c.removeListener(Change.class, this);
150             if (Js.not(ArrayLikes.length(c.links()))) {
151                 Component.removeClasses(c.unwrap(), c.subs("grouped"));
152             }
153         }
154     }
155 
156     /**
157      * <p>Performs an action on the dispatched event.</p>
158      * <p>This method fires {@link Uncheck} event from itself if the dispatched event is from 
159      * a different widget and they both are checked.</p>
160      * @param evt The event dispatched to this listener.
161      * @since 1.0
162      */
163     public void onEvent(Change evt) {
164         Checkable c = (Checkable)Event.source(evt);
165         if (c.checked() && c != this && checked()) {
166             fire(new Uncheck(unwrap()));
167         }
168     }
169 
170     /**
171      * <p>Performs an action on the dispatched event.</p>
172      * <p>This method fires {@link Check} event from itself if it is not checked.</p>
173      * @param evt The event dispatched to this listener.
174      * @since 1.0
175      */
176     public void onEvent(Click evt) {
177         if (!checked()) {
178             exec(new Check(unwrap()));
179         }
180     }
181 
182     /**
183      * <p>Performs an action on the dispatched event.</p>
184      * <p>This method fires {@link Change} event from itself and expands CSS sub selector "checked" 
185      * to its component if it is not checked.</p>
186      * @param evt The event dispatched to this listener.
187      * @since 1.0
188      */
189     public void onEvent(Check evt) {
190         if (!checked()) {
191             Component e = unwrap();
192             ini(e).var(CHECKED, true);
193             Component.addClasses(e, subs("checked"));
194             fire(new Change(e));
195         }
196     }
197 
198     /**
199      * <p>Performs an action on the dispatched event.</p>
200      * <p>This method fires {@link Change} event from itself and removes CSS sub selector "checked" 
201      * from its component if it is checked.</p>
202      * @param evt The event dispatched to this listener.
203      * @since 1.0
204      */
205     public void onEvent(Uncheck evt) {
206         if (checked()) {
207             Component e = unwrap();
208             ObjectLikes.delete(ini(e), CHECKED);
209             Component.removeClasses(e, subs("checked"));
210             fire(new Change(e));
211         }
212     }
213 }