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