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 * |