001    // Copyright May 4, 2006 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    package org.apache.tapestry.dojo.form;
015    
016    import org.apache.tapestry.*;
017    import org.apache.tapestry.engine.DirectServiceParameter;
018    import org.apache.tapestry.engine.IEngineService;
019    import org.apache.tapestry.engine.ILink;
020    import org.apache.tapestry.form.ValidatableField;
021    import org.apache.tapestry.form.ValidatableFieldSupport;
022    import org.apache.tapestry.json.IJSONWriter;
023    import org.apache.tapestry.json.JSONObject;
024    import org.apache.tapestry.services.DataSqueezer;
025    import org.apache.tapestry.valid.ValidatorException;
026    
027    import java.util.ArrayList;
028    import java.util.HashMap;
029    import java.util.List;
030    import java.util.Map;
031    
032    /**
033     * An html field similar to a <code>select</code> input field that 
034     * is wrapped by a dojo ComboBox widget.
035     * 
036     * This component uses the {@link IAutocompleteModel} to retrieve and match against
037     * selected values.
038     * 
039     * @author jkuhnert
040     */
041    public abstract class Autocompleter extends AbstractFormWidget implements ValidatableField, IJSONRender, IDirect
042    {
043        // mode, can be remote or local (local being from html rendered option elements)
044        private static final String MODE_REMOTE = "remote";
045        private static final String MODE_LOCAL = "local";    
046        
047        /**
048         * 
049         * {@inheritDoc}
050         */
051        protected void renderFormWidget(IMarkupWriter writer, IRequestCycle cycle)
052        {
053            IAutocompleteModel model = getModel();
054            if (model == null)
055                throw Tapestry.createRequiredParameterException(this, "model");
056            
057            Object value = getValue();
058            Object key = value != null && !"".equals(value.toString()) ? model.getPrimaryKey(value) : null;
059            
060            renderDelegatePrefix(writer, cycle);
061            
062            writer.begin("select");
063            writer.attribute("name", getName());
064            writer.attribute("autocomplete", "off"); // turn off native html autocomplete
065            
066            if (isDisabled())
067                writer.attribute("disabled", "disabled");
068            
069            renderIdAttribute(writer, cycle);
070            
071            renderDelegateAttributes(writer, cycle);
072            
073            getValidatableFieldSupport().renderContributions(this, writer, cycle);
074            
075            // Apply informal attributes.
076            renderInformalParameters(writer, cycle);
077            
078            writer.print(" ");
079            
080            if (isLocal()) 
081            {
082                List list = model.getValues("");
083                for (int i=0; i<list.size(); i++) 
084                {
085                    Object optionKey = model.getPrimaryKey(list.get(i));
086    
087                    writer.begin("option");
088                    writer.attribute("value", getDataSqueezer().squeeze(optionKey));
089    
090                    if (optionKey!=null && optionKey.equals(key))
091                        writer.attribute("selected", "selected");
092                    
093                    writer.print(model.getLabelFor(list.get(i)));
094                    writer.end();
095                }
096            }
097            
098            writer.end();
099            renderDelegateSuffix(writer, cycle);
100            
101            Map parms = new HashMap();
102            parms.put("id", getClientId());
103            
104            JSONObject json = new JSONObject();
105            if (!isLocal())
106            {
107                ILink link = getDirectService().getLink(true, new DirectServiceParameter(this));
108                json.put("dataUrl", link.getURL() + "&filter=%{searchString}");
109            }
110            
111            json.put("mode", isLocal() ? MODE_LOCAL : MODE_REMOTE);
112            json.put("widgetId", getName());
113            json.put("name", getName());
114            json.put("searchDelay", getSearchDelay());
115            json.put("fadeTime", getFadeTime());
116            json.put("maxListLength", getMaxListLength());
117            json.put("forceValidOption", isForceValidOption());
118            json.put("disabled", isDisabled());
119            json.put("autoComplete", getAutoCompleteField());
120            
121            json.put("value", key != null ? getDataSqueezer().squeeze(key) : "");
122            json.put("label", value != null ? model.getLabelFor(value) : "");
123            
124            parms.put("props", json.toString());
125            parms.put("form", getForm().getName());
126            parms.put("widget", this);
127            
128            PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, this);
129            getScript().execute(this, cycle, prs, parms);
130        }
131        
132        /**
133         * {@inheritDoc}
134         */
135        public void renderComponent(IJSONWriter writer, IRequestCycle cycle)
136        {
137            IAutocompleteModel model = getModel();
138            
139            if (model == null)
140                throw Tapestry.createRequiredParameterException(this, "model");
141            
142            List filteredValues = model.getValues(getFilter());
143            
144            if (filteredValues == null)
145                return;
146            
147            Object key = null;
148            String label = null;
149            
150            JSONObject json = writer.object();
151            
152            for (int i=0; i < filteredValues.size(); i++) {
153                Object value = filteredValues.get(i);
154                
155                key = model.getPrimaryKey(value);
156                label = model.getLabelFor(value);
157                
158                json.put(getDataSqueezer().squeeze(key), label );
159            }
160            
161        }
162        
163        /**
164         * @see org.apache.tapestry.form.AbstractFormComponent#rewindFormComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
165         */
166        protected void rewindFormWidget(IMarkupWriter writer, IRequestCycle cycle)
167        {
168            String value = cycle.getParameter(getName());
169            
170            Object object = null;
171            
172            try
173            {
174                if (value != null && value.length() > 0)
175                    object = getModel().getValue(getDataSqueezer().unsqueeze(value));
176                
177                getValidatableFieldSupport().validate(this, writer, cycle, object);
178                
179                setValue(object);
180            }
181            catch (ValidatorException e)
182            {
183                getForm().getDelegate().record(e);
184            }
185        }
186        
187        /** 
188         * {@inheritDoc}
189         */
190        public boolean isStateful()
191        {
192            return true;
193        }
194        
195        /**
196         * Triggerd by using filterOnChange logic.
197         * 
198         * {@inheritDoc}
199         */
200        public void trigger(IRequestCycle cycle)
201        {
202            setFilter(cycle.getParameter("filter"));
203        }
204        
205        public abstract IAutocompleteModel getModel();
206        
207        /** How long to wait(in ms) before searching after input is received. */
208        public abstract int getSearchDelay();
209        
210        /** The duration(in ms) of the fade effect of list going away. */
211        public abstract int getFadeTime();
212        
213        /** The maximum number of items displayed in select list before the scrollbar is activated. */
214        public abstract int getMaxListLength();
215        
216        /** Forces select to only allow valid option strings. */
217        public abstract boolean isForceValidOption();
218        
219        /** Forces select to work in local mode (no xhr). */
220        public abstract boolean isLocal();    
221        
222        /** @since 2.2 * */
223        public abstract Object getValue();
224    
225        /** @since 2.2 * */
226        public abstract void setValue(Object value);
227        
228        /** @since 4.1 */
229        public abstract void setFilter(String value);
230        
231        /** @since 4.1 */
232        public abstract String getFilter();
233    
234        /** @since 4.1.4 */
235        public abstract boolean getAutoCompleteField();
236        
237        /** Injected. */
238        public abstract DataSqueezer getDataSqueezer();
239        
240        /**
241         * Injected.
242         */
243        public abstract ValidatableFieldSupport getValidatableFieldSupport();
244    
245        /**
246         * Injected.
247         */
248        public abstract IEngineService getDirectService();
249        
250        /**
251         * Injected.
252         */
253        public abstract IScript getScript();
254        
255        /**
256         * @see org.apache.tapestry.form.AbstractFormComponent#isRequired()
257         */
258        public boolean isRequired()
259        {
260            return getValidatableFieldSupport().isRequired(this);
261        }
262    
263        /** 
264         * {@inheritDoc}
265         */
266        public List getUpdateComponents()
267        {
268            List comps = new ArrayList();
269            comps.add(getClientId());
270            
271            return comps;
272        }
273        
274        /** 
275         * {@inheritDoc}
276         */
277        public boolean isAsync()
278        {
279            return true;
280        }
281        
282        /** 
283         * {@inheritDoc}
284         */
285        public boolean isJson()
286        {
287            return true;
288        }
289    }