web/data/cubicweb.widgets.js
changeset 6448 8590d82e9b1b
parent 6372 4c3e2a92e340
child 6450 c23639f26ec6
equal deleted inserted replaced
6447:f5d1b1025702 6448:8590d82e9b1b
    54 
    54 
    55 function getJSON(url, data, callback) {
    55 function getJSON(url, data, callback) {
    56     return jQuery.get(url, data, callback, 'json');
    56     return jQuery.get(url, data, callback, 'json');
    57 }
    57 }
    58 
    58 
       
    59 
       
    60 (function ($) {
       
    61     var defaultSettings = {
       
    62         initialvalue: '',
       
    63         multiple: false,
       
    64         mustMatch: false,
       
    65         delay: 50,
       
    66         limit: 50
       
    67     };
       
    68     function split(val) { return val.split( /\s*,\s*/ ); }
       
    69     function extractLast(term) { return split(term).pop(); }
       
    70     function allButLast(val) {
       
    71         var terms = split(val);
       
    72         terms.pop();
       
    73         return terms;
       
    74     }
       
    75 
       
    76     var methods = {
       
    77         __init__: function(suggestions, options) {
       
    78             return this.each(function() {
       
    79                 // here, `this` refers to the DOM element (e.g. input) being wrapped
       
    80                 // by cwautomplete plugin
       
    81                 var instanceData = $(this).data('cwautocomplete');
       
    82                 if (instanceData) {
       
    83                     // already initialized
       
    84                     return;
       
    85                 }
       
    86                 var settings = $.extend({}, defaultSettings, options);
       
    87                 instanceData =  {
       
    88                     initialvalue: settings.initialvalue,
       
    89                     userInput: this,
       
    90                     hiddenInput: null
       
    91                 };
       
    92                 var hiHandlers = methods.hiddenInputHandlers;
       
    93                 $(this).data('cwautocomplete', instanceData);
       
    94                 $.ui.autocomplete.prototype._search = methods.search;
       
    95                 if (settings.multiple) {
       
    96                     $.ui.autocomplete.filter = methods.multiple.makeFilter(this);
       
    97                     $(this).bind({
       
    98                         autocompleteselect: methods.multiple.select,
       
    99                         autocompletefocus: methods.multiple.focus,
       
   100                         keydown: methods.multiple.keydown
       
   101                         });
       
   102                 }
       
   103                 if ($.isArray(suggestions)) { // precomputed list of suggestions
       
   104                     settings.source = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions);
       
   105                 } else { // url to call each time something is typed
       
   106                     settings.source = function(request, response) {
       
   107                         var d = loadRemote(suggestions, {q: request.term, limit: settings.limit}, 'POST');
       
   108                         d.addCallback(function (suggestions) {
       
   109                             suggestions = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions);
       
   110                             response(suggestions);
       
   111                         });
       
   112                     };
       
   113                 }
       
   114                 $(this).autocomplete(settings);
       
   115                 if (settings.mustMach) {
       
   116                     $(this).keypress(methods.ensureExactMatch);
       
   117                 }
       
   118             });
       
   119         },
       
   120 
       
   121         multiple: {
       
   122             focus: function() {
       
   123                 // prevent value inserted on focus
       
   124                 return false;
       
   125             },
       
   126             select: function(event, ui) {
       
   127                 var terms = allButLast(this.value);
       
   128                 // add the selected item
       
   129                 terms.push(ui.item.value);
       
   130                 // add placeholder to get the comma-and-space at the end
       
   131                 terms.push("");
       
   132                 this.value = terms.join( ", " );
       
   133                 return false;
       
   134             },
       
   135             keydown: function(evt) {
       
   136                 if ($(this).data('autocomplete').menu.active && evt.keyCode == $.ui.keyCode.TAB) {
       
   137                     evt.preventDefault();
       
   138                 }
       
   139             },
       
   140             makeFilter: function(userInput) {
       
   141                 return function(array, term) {
       
   142                     // remove already entered terms from suggestion list
       
   143                     array = cw.utils.difference(array, allButLast(userInput.value));
       
   144                     var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
       
   145                     return $.grep( array, function(value) {
       
   146                         return matcher.test( value.label || value.value || value );
       
   147                     });
       
   148                 };
       
   149             }
       
   150         },
       
   151 
       
   152         search: function(value) {
       
   153             this.element.addClass("ui-autocomplete-loading");
       
   154             if (this.options.multiple) {
       
   155                 value = extractLast(value);
       
   156             }
       
   157             this.source({term: value}, this.response);
       
   158         },
       
   159         ensureExactMatch: function(evt) {
       
   160             var instanceData = $(this).data('cwautocomplete');
       
   161             if (evt.keyCode == $.ui.keyCode.ENTER || evt.keyCode == $.ui.keyCode.TAB) {
       
   162                 var validChoices = $.map($('ul.ui-autocomplete li'),
       
   163                                          function(li) {return $(li).text();});
       
   164                 if ($.inArray($(instanceData.userInput).val(), validChoices) == -1) {
       
   165                     $(instanceData.userInput).val('');
       
   166                     $(instanceData.hiddenInput).val(instanceData.initialvalue || '');
       
   167                 }
       
   168             }
       
   169         },
       
   170 
       
   171         hiddenInputHandlers: {
       
   172             /**
       
   173              * `hiddenInputHandlers` defines all methods specific to handle the
       
   174              * hidden input created along the standard text input.
       
   175              * An hiddenInput is necessary when displayed suggestions are
       
   176              * different from actual values to submit.
       
   177              * Imagine an autocompletion widget to choose among a list of CWusers.
       
   178              * Suggestions would be the list of logins, but actual values would
       
   179              * be the corresponding eids.
       
   180              * To handle such cases, suggestions list should be a list of JS objects
       
   181              * with two `label` and `value` properties.
       
   182              **/
       
   183             suggestionSelected: function(evt, ui) {
       
   184                 var instanceData = $(this).data('cwautocomplete');
       
   185                 instanceData.hiddenInput.value = ui.item.value;
       
   186                 instanceData.value = ui.item.label;
       
   187                 return false; // stop propagation
       
   188             },
       
   189 
       
   190             suggestionFocusChanged: function(evt, ui) {
       
   191                 var instanceData = $(this).data('cwautocomplete');
       
   192                 instanceData.userInput.value = ui.item.label;
       
   193                 return false; // stop propagation
       
   194             },
       
   195 
       
   196             needsHiddenInput: function(suggestions) {
       
   197                 return suggestions[0].label !== undefined;
       
   198             },
       
   199 
       
   200             initializeHiddenInput: function(instanceData) {
       
   201                 var userInput = instanceData.userInput;
       
   202                 var hiddenInput = INPUT({
       
   203                     type: "hidden",
       
   204                     name: userInput.name,
       
   205                     value: userInput.value
       
   206                 });
       
   207                 $(userInput).removeAttr('name').after(hiddenInput);
       
   208                 instanceData.hiddenInput = hiddenInput;
       
   209                 $(userInput).bind({
       
   210                     autocompleteselect: methods.hiddenInputHandlers.suggestionSelected,
       
   211                     autocompletefocus: methods.hiddenInputHandlers.suggestionFocusChanged
       
   212                 });
       
   213             },
       
   214 
       
   215             /*
       
   216              * internal convenience function: old jquery plugin accepted to be fed
       
   217              * with a list of couples (value, label). The new (jquery-ui) autocomplete
       
   218              * plugin expects a list of objects with "value" and "label" properties.
       
   219              *
       
   220              * This function converts the old format to the new one.
       
   221              */
       
   222             checkSuggestionsDataFormat: function(instanceData, suggestions) {
       
   223                 // check for old (value, label) format
       
   224                 if ($.isArray(suggestions) && suggestions.length &&
       
   225                     $.isArray(suggestions[0]) && suggestions[0].length == 2) {
       
   226                     cw.log('[3.10] autocomplete init func should return {label,value} dicts instead of lists');
       
   227                     suggestions = $.map(suggestions, function(sugg) {
       
   228                         return {value: sugg[0], label: sugg[1]};
       
   229                     });
       
   230                 }
       
   231                 var hiHandlers = methods.hiddenInputHandlers;
       
   232                 if (suggestions.length && hiHandlers.needsHiddenInput(suggestions)
       
   233                     && !instanceData.hiddenInput) {
       
   234                     hiHandlers.initializeHiddenInput(instanceData);
       
   235                     hiHandlers.fixUserInputInitialValue(instanceData, suggestions);
       
   236                 }
       
   237                 // otherwise, assume data shape is correct
       
   238                 return suggestions;
       
   239             },
       
   240 
       
   241             fixUserInputInitialValue: function(instanceData, suggestions) {
       
   242                 // called when the data is loaded to reset the correct displayed
       
   243                 // value in the visible input field (typically replacing an eid
       
   244                 // by a displayable value)
       
   245                 var curvalue = instanceData.userInput.value;
       
   246                 if (!curvalue) {
       
   247                     return;
       
   248                 }
       
   249                 for (var i=0, length=suggestions.length; i < length; i++) {
       
   250                     var sugg = suggestions[i];
       
   251                     if (sugg.value == curvalue) {
       
   252                         instanceData.userInput.value = sugg.label;
       
   253                         return;
       
   254                     }
       
   255                 }
       
   256             }
       
   257         }
       
   258     };
       
   259 
       
   260     $.fn.cwautocomplete = function(data, options) {
       
   261         return methods.__init__.apply(this, [data, options]);
       
   262     };
       
   263 })(jQuery);
       
   264 
       
   265 
    59 Widgets.SuggestField = defclass('SuggestField', null, {
   266 Widgets.SuggestField = defclass('SuggestField', null, {
    60     __init__: function(node, options) {
   267     __init__: function(node, options) {
    61         var multi = node.getAttribute('cubicweb:multi') || "no";
       
    62         options = options || {};
   268         options = options || {};
       
   269         var multi = node.getAttribute('cubicweb:multi');
    63         options.multiple = (multi == "yes") ? true: false;
   270         options.multiple = (multi == "yes") ? true: false;
    64         var dataurl = node.getAttribute('cubicweb:dataurl');
   271         var d = loadRemote(node.getAttribute('cubicweb:dataurl'));
    65         var method = postJSON;
   272         d.addCallback(function(data) {
    66         if (options.method == 'get') {
   273             $(node).cwautocomplete(data, options);
    67             method = function(url, data, callback) {
       
    68                 // We can't rely on jQuery.getJSON because the server
       
    69                 // might set the Content-Type's response header to 'text/plain'
       
    70                 jQuery.get(url, data, function(response) {
       
    71                     callback(cw.evalJSON(response));
       
    72                 });
       
    73             };
       
    74         }
       
    75         var self = this; // closure
       
    76         method(dataurl, null, function(data) {
       
    77             // in case we received a list of couple, we assume that the first
       
    78             // element is the real value to be sent, and the second one is the
       
    79             // value to be displayed
       
    80             if (data.length && data[0].length == 2) {
       
    81                 options.formatItem = function(row) {
       
    82                     return row[1];
       
    83                 };
       
    84                 self.hideRealValue(node);
       
    85                 self.setCurrentValue(node, data);
       
    86             }
       
    87             jQuery(node).autocomplete(data, options);
       
    88         });
   274         });
    89     },
       
    90 
       
    91     hideRealValue: function(node) {
       
    92         var hidden = INPUT({
       
    93             'type': "hidden",
       
    94             'name': node.name,
       
    95             'value': node.value
       
    96         });
       
    97         node.parentNode.appendChild(hidden);
       
    98         // remove 'name' attribute from visible input so that it is not submitted
       
    99         // and set correct value in the corresponding hidden field
       
   100         jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
       
   101             hidden.value = row[0];
       
   102         });
       
   103     },
       
   104 
       
   105     setCurrentValue: function(node, data) {
       
   106         // called when the data is loaded to reset the correct displayed
       
   107         // value in the visible input field (typically replacing an eid
       
   108         // by a displayable value)
       
   109         var curvalue = node.value;
       
   110         if (!node.value) {
       
   111             return;
       
   112         }
       
   113         for (var i = 0, length = data.length; i < length; i++) {
       
   114             var row = data[i];
       
   115             if (row[0] == curvalue) {
       
   116                 node.value = row[1];
       
   117                 return;
       
   118             }
       
   119         }
       
   120     }
   275     }
   121 });
   276 });
   122 
   277 
   123 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], {
   278 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], {
   124 
   279 
   125     __init__: function(node) {
   280     __init__: function(node) {
   126         Widgets.SuggestField.__init__(this, node, {
   281         Widgets.SuggestField.__init__(this, node, {
   127             method: 'get'
   282             method: 'get' // XXX
   128         });
   283         });
   129     }
   284     }
   130 
   285 
   131 });
   286 });
   132 
   287 
   133 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
   288 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
   134 
       
   135     __init__: function(node) {
   289     __init__: function(node) {
   136         Widgets.SuggestField.__init__(this, node, {
   290         Widgets.SuggestField.__init__(this, node, {
   137             mustMatch: true
   291             mustMatch: true
   138         });
   292         });
   139     }
   293     }
   140 
   294 });
   141 });
   295 
   142 //remote version of RestrictedSuggestField
   296 //remote version of RestrictedSuggestField
   143 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
   297 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
   144     __init__: function(node, options) {
   298     __init__: function(node, options) {
   145         var self = this;
   299         var self = this;
   146         var multi = "no";
       
   147         options = options || {};
   300         options = options || {};
   148         options.max = 50;
       
   149         options.delay = 50;
   301         options.delay = 50;
   150         options.cacheLength = 0;
       
   151         options.mustMatch = true;
       
   152         // multiple selection not supported yet (still need to formalize correctly
   302         // multiple selection not supported yet (still need to formalize correctly
   153         // initial values / display values)
   303         // initial values / display values)
   154         var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
   304         var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
   155         if (!initialvalue) {
   305         if (!initialvalue) {
   156             initialvalue = node.value;
   306             initialvalue = node.value;
   157         }
   307         }
   158         options = jQuery.extend({
   308         options.initialvalue = initialvalue;
   159             dataType: 'json',
   309         Widgets.SuggestField.__init__(this, node, options);
   160             multiple: (multi == "yes") ? true: false,
   310     }
   161             parse: this.parseResult
       
   162         },
       
   163         options);
       
   164         var dataurl = node.getAttribute('cubicweb:dataurl');
       
   165         // remove 'name' from original input and add the hidden one that will
       
   166         // store the actual value
       
   167         var hidden = INPUT({
       
   168             'type': "hidden",
       
   169             'name': node.name,
       
   170             'value': initialvalue
       
   171         });
       
   172         node.parentNode.appendChild(hidden);
       
   173         jQuery(node).bind('result', {
       
   174             hinput: hidden,
       
   175             input: node
       
   176         },
       
   177         self.hideRealValue).removeAttr('name').autocomplete(dataurl, options);
       
   178     },
       
   179 
       
   180     hideRealValue: function(evt, data, value) {
       
   181         if (!value) {
       
   182             value = "";
       
   183         }
       
   184         evt.data.hinput.value = value;
       
   185     },
       
   186 
       
   187     /*
       
   188      * @param data: a list of couple (value, label) to fill the suggestion list,
       
   189      *              (returned by CW through AJAX)
       
   190      */
       
   191     parseResult: function(data) {
       
   192         var parsed = [];
       
   193         for (var i = 0; i < data.length; i++) {
       
   194             var value = '' + data[i][0]; // a string is required later by jquery.autocomplete.js
       
   195             var label = data[i][1];
       
   196             parsed[parsed.length] = {
       
   197                 data: [label],
       
   198                 value: value,
       
   199                 result: label
       
   200             };
       
   201         };
       
   202         return parsed;
       
   203     }
       
   204 
       
   205 });
   311 });
   206 
   312 
   207 /**
   313 /**
   208  * .. class:: Widgets.SuggestForm
   314  * .. class:: Widgets.SuggestForm
   209  *
   315  *