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            
120            json.put("value", key != null ? getDataSqueezer().squeeze(key) : "");
121            json.put("label", value != null ? model.getLabelFor(value) : "");
122            
123            parms.put("props", json.toString());
124            parms.put("form", getForm().getName());
125            parms.put("widget", this);
126            
127            PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, this);
128            getScript().execute(this, cycle, prs, parms);
129        }
130        
131        /**
132         * {@inheritDoc}
133         */
134        public void renderComponent(IJSONWriter writer, IRequestCycle cycle)
135        {
136            IAutocompleteModel model = getModel();
137            
138            if (model == null)
139                throw Tapestry.createRequiredParameterException(this, "model");
140            
141            List filteredValues = model.getValues(getFilter());
142            
143            if (filteredValues == null)
144                return;
145            
146            Object key = null;
147            String label = null;
148            
149            JSONObject json = writer.object();
150            
151            for (int i=0; i < filteredValues.size(); i++) {
152                Object value = filteredValues.get(i);
153                
154                key = model.getPrimaryKey(value);
155                label = model.getLabelFor(value);
156                
157                json.put(getDataSqueezer().squeeze(key), label );
158            }
159            
160        }
161        
162        /**
163         * @see org.apache.tapestry.form.AbstractFormComponent#rewindFormComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
164         */
165        protected void rewindFormWidget(IMarkupWriter writer, IRequestCycle cycle)
166        {
167            String value = cycle.getParameter(getName());
168            
169            Object object = null;
170            
171            try
172            {
173                if (value != null && value.length() > 0)
174                    object = getModel().getValue(getDataSqueezer().unsqueeze(value));
175                
176                getValidatableFieldSupport().validate(this, writer, cycle, object);
177                
178                setValue(object);
179            }
180            catch (ValidatorException e)
181            {
182                getForm().getDelegate().record(e);
183            }
184        }
185        
186        /** 
187         * {@inheritDoc}
188         */
189        public boolean isStateful()
190        {
191            return true;
192        }
193        
194        /**
195         * Triggerd by using filterOnChange logic.
196         * 
197         * {@inheritDoc}
198         */
199        public void trigger(IRequestCycle cycle)
200        {
201            setFilter(cycle.getParameter("filter"));
202        }
203        
204        public abstract IAutocompleteModel getModel();
205        
206        /** How long to wait(in ms) before searching after input is received. */
207        public abstract int getSearchDelay();
208        
209        /** The duration(in ms) of the fade effect of list going away. */
210        public abstract int getFadeTime();
211        
212        /** The maximum number of items displayed in select list before the scrollbar is activated. */
213        public abstract int getMaxListLength();
214        
215        /** Forces select to only allow valid option strings. */
216        public abstract boolean isForceValidOption();
217        
218        /** Forces select to work in local mode (no xhr). */
219        public abstract boolean isLocal();    
220        
221        /** @since 2.2 * */
222        public abstract Object getValue();
223    
224        /** @since 2.2 * */
225        public abstract void setValue(Object value);
226        
227        /** @since 4.1 */
228        public abstract void setFilter(String value);
229        
230        /** @since 4.1 */
231        public abstract String getFilter();
232        
233        /** Injected. */
234        public abstract DataSqueezer getDataSqueezer();
235        
236        /**
237         * Injected.
238         */
239        public abstract ValidatableFieldSupport getValidatableFieldSupport();
240    
241        /**
242         * Injected.
243         */
244        public abstract IEngineService getDirectService();
245        
246        /**
247         * Injected.
248         */
249        public abstract IScript getScript();
250        
251        /**
252         * @see org.apache.tapestry.form.AbstractFormComponent#isRequired()
253         */
254        public boolean isRequired()
255        {
256            return getValidatableFieldSupport().isRequired(this);
257        }
258    
259        /** 
260         * {@inheritDoc}
261         */
262        public List getUpdateComponents()
263        {
264            List comps = new ArrayList();
265            comps.add(getClientId());
266            
267            return comps;
268        }
269        
270        /** 
271         * {@inheritDoc}
272         */
273        public boolean isAsync()
274        {
275            return true;
276        }
277        
278        /** 
279         * {@inheritDoc}
280         */
281        public boolean isJson()
282        {
283            return true;
284        }
285    }