web/data/cubicweb.widgets.js
changeset 6448 8590d82e9b1b
parent 6372 4c3e2a92e340
child 6450 c23639f26ec6
--- a/web/data/cubicweb.widgets.js	Mon Oct 11 17:46:22 2010 +0200
+++ b/web/data/cubicweb.widgets.js	Mon Oct 11 19:12:59 2010 +0200
@@ -56,67 +56,222 @@
     return jQuery.get(url, data, callback, 'json');
 }
 
+
+(function ($) {
+    var defaultSettings = {
+        initialvalue: '',
+        multiple: false,
+        mustMatch: false,
+        delay: 50,
+        limit: 50
+    };
+    function split(val) { return val.split( /\s*,\s*/ ); }
+    function extractLast(term) { return split(term).pop(); }
+    function allButLast(val) {
+        var terms = split(val);
+        terms.pop();
+        return terms;
+    }
+
+    var methods = {
+        __init__: function(suggestions, options) {
+            return this.each(function() {
+                // here, `this` refers to the DOM element (e.g. input) being wrapped
+                // by cwautomplete plugin
+                var instanceData = $(this).data('cwautocomplete');
+                if (instanceData) {
+                    // already initialized
+                    return;
+                }
+                var settings = $.extend({}, defaultSettings, options);
+                instanceData =  {
+                    initialvalue: settings.initialvalue,
+                    userInput: this,
+                    hiddenInput: null
+                };
+                var hiHandlers = methods.hiddenInputHandlers;
+                $(this).data('cwautocomplete', instanceData);
+                $.ui.autocomplete.prototype._search = methods.search;
+                if (settings.multiple) {
+                    $.ui.autocomplete.filter = methods.multiple.makeFilter(this);
+                    $(this).bind({
+                        autocompleteselect: methods.multiple.select,
+                        autocompletefocus: methods.multiple.focus,
+                        keydown: methods.multiple.keydown
+                        });
+                }
+                if ($.isArray(suggestions)) { // precomputed list of suggestions
+                    settings.source = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions);
+                } else { // url to call each time something is typed
+                    settings.source = function(request, response) {
+                        var d = loadRemote(suggestions, {q: request.term, limit: settings.limit}, 'POST');
+                        d.addCallback(function (suggestions) {
+                            suggestions = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions);
+                            response(suggestions);
+                        });
+                    };
+                }
+                $(this).autocomplete(settings);
+                if (settings.mustMach) {
+                    $(this).keypress(methods.ensureExactMatch);
+                }
+            });
+        },
+
+        multiple: {
+            focus: function() {
+                // prevent value inserted on focus
+                return false;
+            },
+            select: function(event, ui) {
+                var terms = allButLast(this.value);
+                // add the selected item
+                terms.push(ui.item.value);
+                // add placeholder to get the comma-and-space at the end
+                terms.push("");
+                this.value = terms.join( ", " );
+                return false;
+            },
+            keydown: function(evt) {
+                if ($(this).data('autocomplete').menu.active && evt.keyCode == $.ui.keyCode.TAB) {
+                    evt.preventDefault();
+                }
+            },
+            makeFilter: function(userInput) {
+                return function(array, term) {
+                    // remove already entered terms from suggestion list
+                    array = cw.utils.difference(array, allButLast(userInput.value));
+                    var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
+                    return $.grep( array, function(value) {
+                        return matcher.test( value.label || value.value || value );
+                    });
+                };
+            }
+        },
+
+        search: function(value) {
+            this.element.addClass("ui-autocomplete-loading");
+            if (this.options.multiple) {
+                value = extractLast(value);
+            }
+            this.source({term: value}, this.response);
+        },
+        ensureExactMatch: function(evt) {
+            var instanceData = $(this).data('cwautocomplete');
+            if (evt.keyCode == $.ui.keyCode.ENTER || evt.keyCode == $.ui.keyCode.TAB) {
+                var validChoices = $.map($('ul.ui-autocomplete li'),
+                                         function(li) {return $(li).text();});
+                if ($.inArray($(instanceData.userInput).val(), validChoices) == -1) {
+                    $(instanceData.userInput).val('');
+                    $(instanceData.hiddenInput).val(instanceData.initialvalue || '');
+                }
+            }
+        },
+
+        hiddenInputHandlers: {
+            /**
+             * `hiddenInputHandlers` defines all methods specific to handle the
+             * hidden input created along the standard text input.
+             * An hiddenInput is necessary when displayed suggestions are
+             * different from actual values to submit.
+             * Imagine an autocompletion widget to choose among a list of CWusers.
+             * Suggestions would be the list of logins, but actual values would
+             * be the corresponding eids.
+             * To handle such cases, suggestions list should be a list of JS objects
+             * with two `label` and `value` properties.
+             **/
+            suggestionSelected: function(evt, ui) {
+                var instanceData = $(this).data('cwautocomplete');
+                instanceData.hiddenInput.value = ui.item.value;
+                instanceData.value = ui.item.label;
+                return false; // stop propagation
+            },
+
+            suggestionFocusChanged: function(evt, ui) {
+                var instanceData = $(this).data('cwautocomplete');
+                instanceData.userInput.value = ui.item.label;
+                return false; // stop propagation
+            },
+
+            needsHiddenInput: function(suggestions) {
+                return suggestions[0].label !== undefined;
+            },
+
+            initializeHiddenInput: function(instanceData) {
+                var userInput = instanceData.userInput;
+                var hiddenInput = INPUT({
+                    type: "hidden",
+                    name: userInput.name,
+                    value: userInput.value
+                });
+                $(userInput).removeAttr('name').after(hiddenInput);
+                instanceData.hiddenInput = hiddenInput;
+                $(userInput).bind({
+                    autocompleteselect: methods.hiddenInputHandlers.suggestionSelected,
+                    autocompletefocus: methods.hiddenInputHandlers.suggestionFocusChanged
+                });
+            },
+
+            /*
+             * internal convenience function: old jquery plugin accepted to be fed
+             * with a list of couples (value, label). The new (jquery-ui) autocomplete
+             * plugin expects a list of objects with "value" and "label" properties.
+             *
+             * This function converts the old format to the new one.
+             */
+            checkSuggestionsDataFormat: function(instanceData, suggestions) {
+                // check for old (value, label) format
+                if ($.isArray(suggestions) && suggestions.length &&
+                    $.isArray(suggestions[0]) && suggestions[0].length == 2) {
+                    cw.log('[3.10] autocomplete init func should return {label,value} dicts instead of lists');
+                    suggestions = $.map(suggestions, function(sugg) {
+                        return {value: sugg[0], label: sugg[1]};
+                    });
+                }
+                var hiHandlers = methods.hiddenInputHandlers;
+                if (suggestions.length && hiHandlers.needsHiddenInput(suggestions)
+                    && !instanceData.hiddenInput) {
+                    hiHandlers.initializeHiddenInput(instanceData);
+                    hiHandlers.fixUserInputInitialValue(instanceData, suggestions);
+                }
+                // otherwise, assume data shape is correct
+                return suggestions;
+            },
+
+            fixUserInputInitialValue: function(instanceData, suggestions) {
+                // called when the data is loaded to reset the correct displayed
+                // value in the visible input field (typically replacing an eid
+                // by a displayable value)
+                var curvalue = instanceData.userInput.value;
+                if (!curvalue) {
+                    return;
+                }
+                for (var i=0, length=suggestions.length; i < length; i++) {
+                    var sugg = suggestions[i];
+                    if (sugg.value == curvalue) {
+                        instanceData.userInput.value = sugg.label;
+                        return;
+                    }
+                }
+            }
+        }
+    };
+
+    $.fn.cwautocomplete = function(data, options) {
+        return methods.__init__.apply(this, [data, options]);
+    };
+})(jQuery);
+
+
 Widgets.SuggestField = defclass('SuggestField', null, {
     __init__: function(node, options) {
-        var multi = node.getAttribute('cubicweb:multi') || "no";
         options = options || {};
+        var multi = node.getAttribute('cubicweb:multi');
         options.multiple = (multi == "yes") ? true: false;
-        var dataurl = node.getAttribute('cubicweb:dataurl');
-        var method = postJSON;
-        if (options.method == 'get') {
-            method = function(url, data, callback) {
-                // We can't rely on jQuery.getJSON because the server
-                // might set the Content-Type's response header to 'text/plain'
-                jQuery.get(url, data, function(response) {
-                    callback(cw.evalJSON(response));
-                });
-            };
-        }
-        var self = this; // closure
-        method(dataurl, null, function(data) {
-            // in case we received a list of couple, we assume that the first
-            // element is the real value to be sent, and the second one is the
-            // value to be displayed
-            if (data.length && data[0].length == 2) {
-                options.formatItem = function(row) {
-                    return row[1];
-                };
-                self.hideRealValue(node);
-                self.setCurrentValue(node, data);
-            }
-            jQuery(node).autocomplete(data, options);
+        var d = loadRemote(node.getAttribute('cubicweb:dataurl'));
+        d.addCallback(function(data) {
+            $(node).cwautocomplete(data, options);
         });
-    },
-
-    hideRealValue: function(node) {
-        var hidden = INPUT({
-            'type': "hidden",
-            'name': node.name,
-            'value': node.value
-        });
-        node.parentNode.appendChild(hidden);
-        // remove 'name' attribute from visible input so that it is not submitted
-        // and set correct value in the corresponding hidden field
-        jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
-            hidden.value = row[0];
-        });
-    },
-
-    setCurrentValue: function(node, data) {
-        // called when the data is loaded to reset the correct displayed
-        // value in the visible input field (typically replacing an eid
-        // by a displayable value)
-        var curvalue = node.value;
-        if (!node.value) {
-            return;
-        }
-        for (var i = 0, length = data.length; i < length; i++) {
-            var row = data[i];
-            if (row[0] == curvalue) {
-                node.value = row[1];
-                return;
-            }
-        }
     }
 });
 
@@ -124,84 +279,35 @@
 
     __init__: function(node) {
         Widgets.SuggestField.__init__(this, node, {
-            method: 'get'
+            method: 'get' // XXX
         });
     }
 
 });
 
 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
-
     __init__: function(node) {
         Widgets.SuggestField.__init__(this, node, {
             mustMatch: true
         });
     }
+});
 
-});
 //remote version of RestrictedSuggestField
 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
     __init__: function(node, options) {
         var self = this;
-        var multi = "no";
         options = options || {};
-        options.max = 50;
         options.delay = 50;
-        options.cacheLength = 0;
-        options.mustMatch = true;
         // multiple selection not supported yet (still need to formalize correctly
         // initial values / display values)
         var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
         if (!initialvalue) {
             initialvalue = node.value;
         }
-        options = jQuery.extend({
-            dataType: 'json',
-            multiple: (multi == "yes") ? true: false,
-            parse: this.parseResult
-        },
-        options);
-        var dataurl = node.getAttribute('cubicweb:dataurl');
-        // remove 'name' from original input and add the hidden one that will
-        // store the actual value
-        var hidden = INPUT({
-            'type': "hidden",
-            'name': node.name,
-            'value': initialvalue
-        });
-        node.parentNode.appendChild(hidden);
-        jQuery(node).bind('result', {
-            hinput: hidden,
-            input: node
-        },
-        self.hideRealValue).removeAttr('name').autocomplete(dataurl, options);
-    },
-
-    hideRealValue: function(evt, data, value) {
-        if (!value) {
-            value = "";
-        }
-        evt.data.hinput.value = value;
-    },
-
-    /*
-     * @param data: a list of couple (value, label) to fill the suggestion list,
-     *              (returned by CW through AJAX)
-     */
-    parseResult: function(data) {
-        var parsed = [];
-        for (var i = 0; i < data.length; i++) {
-            var value = '' + data[i][0]; // a string is required later by jquery.autocomplete.js
-            var label = data[i][1];
-            parsed[parsed.length] = {
-                data: [label],
-                value: value,
-                result: label
-            };
-        };
-        return parsed;
+        options.initialvalue = initialvalue;
+        Widgets.SuggestField.__init__(this, node, options);
     }
-
 });
 
 /**