    001    // Copyright 2007, 2008, 2009, 2010, 2011 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
    015    package org.apache.tapestry5.corelib.mixins;
    016
    017    import java.util.Collections;
    018    import java.util.List;
    019
    020    import org.apache.tapestry5.Asset;
    021    import org.apache.tapestry5.BindingConstants;
    022    import org.apache.tapestry5.CSSClassConstants;
    023    import org.apache.tapestry5.ComponentEventCallback;
    024    import org.apache.tapestry5.ComponentResources;
    025    import org.apache.tapestry5.ContentType;
    026    import org.apache.tapestry5.EventConstants;
    027    import org.apache.tapestry5.Field;
    028    import org.apache.tapestry5.Link;
    029    import org.apache.tapestry5.MarkupWriter;
    030    import org.apache.tapestry5.annotations.Environmental;
    031    import org.apache.tapestry5.annotations.Events;
    032    import org.apache.tapestry5.annotations.Import;
    033    import org.apache.tapestry5.annotations.InjectContainer;
    034    import org.apache.tapestry5.annotations.Parameter;
    035    import org.apache.tapestry5.annotations.Path;
    036    import org.apache.tapestry5.annotations.RequestParameter;
    037    import org.apache.tapestry5.internal.util.Holder;
    038    import org.apache.tapestry5.ioc.annotations.Inject;
    039    import org.apache.tapestry5.ioc.services.TypeCoercer;
    040    import org.apache.tapestry5.json.JSONObject;
    041    import org.apache.tapestry5.services.MarkupWriterFactory;
    042    import org.apache.tapestry5.services.ResponseRenderer;
    043    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    044    import org.apache.tapestry5.util.TextStreamResponse;
    045
    046    /**
    047     * A mixin for a text field that allows for autocompletion of text fields. This is based on Prototype's autocompleter
    048     * control.
    049     * <p/>
    050     * The mixin renders an (initially invisible) progress indicator after the field (it will also be after the error icon
    051     * most fields render). The progress indicator is made visible during the request to the server. The mixin then renders
    052     * a &lt;div&gt; that will be filled in on the client side with dynamically obtained selections.
    053     * <p/>
    054     * Multiple selection on the client is enabled by binding the tokens parameter (however, the mixin doesn't help split
    055     * multiple selections up on the server, that is still your code's responsibility).
    056     * <p/>
    057     * The container is responsible for providing an event handler for event "providecompletions". The context will be the
    058     * partial input string sent from the client. The return value should be an array or list of completions, in
    059     * presentation order. I.e.
    060     * <p/>
    061     *
    062     * <pre>
    063     * String[] onProvideCompletionsFromMyField(String input)
    064     * {
    065     *   return . . .;
    066     * }
    067     * </pre>
    068     *
    069     * @tapestrydoc
    070     */
    071    @Import(library =
    072    { "${tapestry.scriptaculous}/controls.js", "autocomplete.js" })
    073    @Events(EventConstants.PROVIDE_COMPLETIONS)
    074    public class Autocomplete
    075    {
    076        static final String EVENT_NAME = "autocomplete";
    077
    078        private static final String PARAM_NAME = "t:input";
    079
    080        /**
    081         * The field component to which this mixin is attached.
    082         */
    083        @InjectContainer
    084        private Field field;
    085
    086        @Inject
    087        private ComponentResources resources;
    088
    089        @Environmental
    090        private JavaScriptSupport jsSupport;
    091
    092        @Inject
    093        private TypeCoercer coercer;
    094
    095        @Inject
    096        private MarkupWriterFactory factory;
    097
    098        @Inject
    099        @Path("${tapestry.spacer-image}")
    100        private Asset spacerImage;
    101
    102        /**
    103         * Overwrites the default minimum characters to trigger a server round trip (the default is 1).
    104         */
    105        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    106        private int minChars;
    107
    108        @Inject
    109        private ResponseRenderer responseRenderer;
    110
    111        /**
    112         * Overrides the default check frequency for determining whether to send a server request. The default is .4
    113         * seconds.
    114         */
    115        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    116        private double frequency;
    117
    118        /**
    119         * If given, then the autocompleter will support multiple input values, seperated by any of the individual
    120         * characters in the string.
    121         */
    122        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    123        private String tokens;
    124
    125        /**
    126         * Mixin afterRender phrase occurs after the component itself. This is where we write the &lt;div&gt; element and
    127         * the JavaScript.
    128         *
    129         * @param writer
    130         */
    131        void afterRender(MarkupWriter writer)
    132        {
    133            String id = field.getClientId();
    134
    135            String menuId = id + ":menu";
    136            String loaderId = id + ":loader";
    137
    138            // The spacer image is used as a placeholder, allowing CSS to determine what image
    139            // is actually displayed.
    140
    141            writer.element("img",
    142
    143            "src", spacerImage.toClientURL(),
    144
    145            "class", "t-autoloader-icon " + CSSClassConstants.INVISIBLE,
    146
    147            "alt", "",
    148
    149            "id", loaderId);
    150            writer.end();
    151
    152            writer.element("div",
    153
    154            "id", menuId,
    155
    156            "class", "t-autocomplete-menu");
    157            writer.end();
    158
    159            Link link = resources.createEventLink(EVENT_NAME);
    160
    161            JSONObject config = new JSONObject();
    162            config.put("paramName", PARAM_NAME);
    163            config.put("indicator", loaderId);
    164
    165            if (resources.isBound("minChars"))
    166                config.put("minChars", minChars);
    167
    168            if (resources.isBound("frequency"))
    169                config.put("frequency", frequency);
    170
    171            if (resources.isBound("tokens"))
    172            {
    173                for (int i = 0; i < tokens.length(); i++)
    174                {
    175                    config.accumulate("tokens", tokens.substring(i, i + 1));
    176                }
    177            }
    178
    179            // Let subclasses do more.
    180            configure(config);
    181
    182            JSONObject spec = new JSONObject("elementId", id, "menuId", menuId, "url", link.toURI()).put("config",
    183                    config);
    184
    185            jsSupport.addInitializerCall("autocompleter", spec);
    186        }
    187
    188        Object onAutocomplete(@RequestParameter(PARAM_NAME)
    189        String input)
    190        {
    191            final Holder<List> matchesHolder = Holder.create();
    192
    193            // Default it to an empty list.
    194
    195            matchesHolder.put(Collections.emptyList());
    196
    197            ComponentEventCallback callback = new ComponentEventCallback()
    198            {
    199                public boolean handleResult(Object result)
    200                {
    201                    List matches = coercer.coerce(result, List.class);
    202
    203                    matchesHolder.put(matches);
    204
    205                    return true;
    206                }
    207            };
    208
    209            resources.triggerEvent(EventConstants.PROVIDE_COMPLETIONS, new Object[]
    210            { input }, callback);
    211
    212            ContentType contentType = responseRenderer.findContentType(this);
    213
    214            MarkupWriter writer = factory.newPartialMarkupWriter(contentType);
    215
    216            generateResponseMarkup(writer, matchesHolder.get());
    217
    218            return new TextStreamResponse(contentType.toString(), writer.toString());
    219        }
    220
    221        /**
    222         * Invoked to allow subclasses to further configure the parameters passed to the JavaScript Ajax.Autocompleter
    223         * options. The values minChars, frequency and tokens my be pre-configured. Subclasses may override this method to
    224         * configure additional features of the Ajax.Autocompleter.
    225         * <p/>
    226         * <p/>
    227         * This implementation does nothing.
    228         *
    229         * @param config
    230         *            parameters object
    231         */
    232        protected void configure(JSONObject config)
    233        {
    234        }
    235
    236        /**
    237         * Generates the markup response that will be returned to the client; this should be an &lt;ul&gt; element with
    238         * nested &lt;li&gt; elements. Subclasses may override this to produce more involved markup (including images and
    239         * CSS class attributes).
    240         *
    241         * @param writer
    242         *            to write the list to
    243         * @param matches
    244         *            list of matching objects, each should be converted to a string
    245         */
    246        protected void generateResponseMarkup(MarkupWriter writer, List matches)
    247        {
    248            writer.element("ul");
    249
    250            for (Object o : matches)
    251            {
    252                writer.element("li");
    253                writer.write(o.toString());
    254                writer.end();
    255            }
    256
    257            writer.end(); // ul
    258        }
    259    }