1 /** |
|
2 * Functions dedicated to widgets. |
|
3 * |
|
4 * :organization: Logilab |
|
5 * :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
6 * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
7 * |
|
8 * |
|
9 */ |
|
10 |
|
11 // widget namespace |
|
12 Widgets = {}; |
|
13 |
|
14 /** |
|
15 * .. function:: buildWidget(wdgnode) |
|
16 * |
|
17 * this function takes a DOM node defining a widget and |
|
18 * instantiates / builds the appropriate widget class |
|
19 */ |
|
20 function buildWidget(wdgnode) { |
|
21 var wdgclass = Widgets[wdgnode.getAttribute('cubicweb:wdgtype')]; |
|
22 if (wdgclass) { |
|
23 return new wdgclass(wdgnode); |
|
24 } |
|
25 return null; |
|
26 } |
|
27 |
|
28 function renderJQueryDatePicker(subject, button_image, date_format, min_date, max_date){ |
|
29 $widget = cw.jqNode(subject); |
|
30 $widget.datepicker({buttonImage: button_image, dateFormat: date_format, |
|
31 firstDay: 1, showOn: "button", buttonImageOnly: true, |
|
32 minDate: min_date, maxDate: max_date}); |
|
33 $widget.change(function(ev) { |
|
34 maxOfId = $(this).data('max-of'); |
|
35 if (maxOfId) { |
|
36 cw.jqNode(maxOfId).datepicker("option", "maxDate", this.value); |
|
37 } |
|
38 minOfId = $(this).data('min-of'); |
|
39 if (minOfId) { |
|
40 cw.jqNode(minOfId).datepicker("option", "minDate", this.value); |
|
41 } |
|
42 }); |
|
43 } |
|
44 |
|
45 /** |
|
46 * .. function:: buildWidgets(root) |
|
47 * |
|
48 * This function is called on load and is in charge to build |
|
49 * JS widgets according to DOM nodes found in the page |
|
50 */ |
|
51 function buildWidgets(root) { |
|
52 root = root || document; |
|
53 jQuery(root).find('.widget').each(function() { |
|
54 if (this.getAttribute('cubicweb:loadtype') == 'auto') { |
|
55 buildWidget(this); |
|
56 } |
|
57 }); |
|
58 } |
|
59 |
|
60 jQuery(document).ready(function() { |
|
61 buildWidgets(); |
|
62 }); |
|
63 |
|
64 function postJSON(url, data, callback) { |
|
65 return jQuery.post(url, data, callback, AJAX_BASE_URL); |
|
66 } |
|
67 |
|
68 function getJSON(url, data, callback) { |
|
69 return jQuery.get(url, data, callback, AJAX_BASE_URL); |
|
70 } |
|
71 |
|
72 |
|
73 (function ($) { |
|
74 var defaultSettings = { |
|
75 initialvalue: '', |
|
76 multiple: false, |
|
77 mustMatch: false, |
|
78 delay: 50, |
|
79 limit: 50 |
|
80 }; |
|
81 function split(val) { return val.split( /\s*,\s*/ ); } |
|
82 function extractLast(term) { return split(term).pop(); } |
|
83 function allButLast(val) { |
|
84 var terms = split(val); |
|
85 terms.pop(); |
|
86 return terms; |
|
87 } |
|
88 |
|
89 var methods = { |
|
90 __init__: function(suggestions, options) { |
|
91 return this.each(function() { |
|
92 // here, `this` refers to the DOM element (e.g. input) being wrapped |
|
93 // by cwautomplete plugin |
|
94 var instanceData = $(this).data('cwautocomplete'); |
|
95 if (instanceData) { |
|
96 // already initialized |
|
97 return; |
|
98 } |
|
99 var settings = $.extend({}, defaultSettings, options); |
|
100 instanceData = { |
|
101 initialvalue: settings.initialvalue, |
|
102 userInput: this, |
|
103 hiddenInput: null |
|
104 }; |
|
105 var hiHandlers = methods.hiddenInputHandlers; |
|
106 $(this).data('cwautocomplete', instanceData); |
|
107 // in case of an existing value, the hidden input must be initialized even if |
|
108 // the value is not changed |
|
109 if (($(instanceData.userInput).attr('cubicweb:initialvalue') !== undefined) && !instanceData.hiddenInput){ |
|
110 hiHandlers.initializeHiddenInput(instanceData); |
|
111 } |
|
112 $.ui.autocomplete.prototype._value = methods._value; |
|
113 $.data(this, 'settings', settings); |
|
114 if (settings.multiple) { |
|
115 $.ui.autocomplete.filter = methods.multiple.makeFilter(this); |
|
116 $(this).bind({ |
|
117 autocompleteselect: methods.multiple.select, |
|
118 autocompletefocus: methods.multiple.focus, |
|
119 keydown: methods.multiple.keydown |
|
120 }); |
|
121 } |
|
122 // XXX katia we dont need it if minLength == 0, but by setting minLength = 0 |
|
123 // we probably break the backward compatibility |
|
124 $(this).bind('blur', methods.blur); |
|
125 if ($.isArray(suggestions)) { // precomputed list of suggestions |
|
126 settings.source = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions); |
|
127 } else { // url to call each time something is typed |
|
128 settings.source = function(request, response) { |
|
129 var d = loadRemote(suggestions, {q: request.term, limit: settings.limit}, 'POST'); |
|
130 d.addCallback(function (suggestions) { |
|
131 suggestions = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions); |
|
132 response(suggestions); |
|
133 if((suggestions.length) == 0){ |
|
134 methods.resetValues(instanceData); |
|
135 } |
|
136 }); |
|
137 }; |
|
138 } |
|
139 $(this).autocomplete(settings); |
|
140 if (settings.mustMatch) { |
|
141 $(this).keypress(methods.ensureExactMatch); |
|
142 } |
|
143 }); |
|
144 }, |
|
145 |
|
146 _value: function() { |
|
147 /* We extend the widget with the ability to lookup and |
|
148 handle several terms at once ('multiple' option). E.g.: |
|
149 toto, titi, tu.... The autocompletion must be |
|
150 performed only on the last of such a list of terms. |
|
151 */ |
|
152 var settings = $(this.element).data('settings'); |
|
153 var value = this.valueMethod.apply( this.element, arguments ); |
|
154 if (settings.multiple & arguments.length === 0) { |
|
155 return extractLast(value); |
|
156 } |
|
157 return value |
|
158 }, |
|
159 |
|
160 multiple: { |
|
161 focus: function() { |
|
162 // prevent value inserted on focus |
|
163 return false; |
|
164 }, |
|
165 select: function(event, ui) { |
|
166 var terms = allButLast(this.value); |
|
167 // add the selected item |
|
168 terms.push(ui.item.value); |
|
169 // add placeholder to get the comma-and-space at the end |
|
170 terms.push(""); |
|
171 this.value = terms.join( ", " ); |
|
172 return false; |
|
173 }, |
|
174 keydown: function(evt) { |
|
175 if (evt.keyCode == $.ui.keyCode.TAB) { |
|
176 evt.preventDefault(); |
|
177 } |
|
178 }, |
|
179 makeFilter: function(userInput) { |
|
180 return function(array, term) { |
|
181 // remove already entered terms from suggestion list |
|
182 array = cw.utils.difference(array, allButLast(userInput.value)); |
|
183 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); |
|
184 return $.grep( array, function(value) { |
|
185 return matcher.test( value.label || value.value || value ); |
|
186 }); |
|
187 }; |
|
188 } |
|
189 }, |
|
190 blur: function(evt){ |
|
191 var instanceData = $(this).data('cwautocomplete'); |
|
192 if($(instanceData.userInput).val().strip().length==0){ |
|
193 methods.resetValues(instanceData); |
|
194 } |
|
195 }, |
|
196 |
|
197 ensureExactMatch: function(evt) { |
|
198 var instanceData = $(this).data('cwautocomplete'); |
|
199 if (evt.keyCode == $.ui.keyCode.ENTER || evt.keyCode == $.ui.keyCode.TAB) { |
|
200 var validChoices = $.map($('ul.ui-autocomplete li'), |
|
201 function(li) {return $(li).text();}); |
|
202 if ($.inArray($(instanceData.userInput).val(), validChoices) == -1) { |
|
203 $(instanceData.userInput).val(''); |
|
204 $(instanceData.hiddenInput).val(instanceData.initialvalue || ''); |
|
205 } |
|
206 } |
|
207 }, |
|
208 |
|
209 resetValues: function(instanceData){ |
|
210 $(instanceData.userInput).val(''); |
|
211 $(instanceData.hiddenInput).val(''); |
|
212 }, |
|
213 |
|
214 |
|
215 hiddenInputHandlers: { |
|
216 /** |
|
217 * `hiddenInputHandlers` defines all methods specific to handle the |
|
218 * hidden input created along the standard text input. |
|
219 * An hiddenInput is necessary when displayed suggestions are |
|
220 * different from actual values to submit. |
|
221 * Imagine an autocompletion widget to choose among a list of CWusers. |
|
222 * Suggestions would be the list of logins, but actual values would |
|
223 * be the corresponding eids. |
|
224 * To handle such cases, suggestions list should be a list of JS objects |
|
225 * with two `label` and `value` properties. |
|
226 **/ |
|
227 suggestionSelected: function(evt, ui) { |
|
228 var instanceData = $(this).data('cwautocomplete'); |
|
229 instanceData.hiddenInput.value = ui.item.value; |
|
230 instanceData.value = ui.item.label; |
|
231 return false; // stop propagation |
|
232 }, |
|
233 |
|
234 suggestionFocusChanged: function(evt, ui) { |
|
235 var instanceData = $(this).data('cwautocomplete'); |
|
236 instanceData.userInput.value = ui.item.label; |
|
237 return false; // stop propagation |
|
238 }, |
|
239 |
|
240 needsHiddenInput: function(suggestions) { |
|
241 return suggestions[0].label !== undefined; |
|
242 }, |
|
243 initializeHiddenInput: function(instanceData) { |
|
244 var userInput = instanceData.userInput; |
|
245 var hiddenInput = INPUT({ |
|
246 type: "hidden", |
|
247 name: userInput.name, |
|
248 // XXX katia : this must be handeled in .SuggestField widget, but |
|
249 // it seems not to be used anymore |
|
250 value: $(userInput).attr('cubicweb:initialvalue') || userInput.value |
|
251 }); |
|
252 $(userInput).removeAttr('name').after(hiddenInput); |
|
253 instanceData.hiddenInput = hiddenInput; |
|
254 $(userInput).bind({ |
|
255 autocompleteselect: methods.hiddenInputHandlers.suggestionSelected, |
|
256 autocompletefocus: methods.hiddenInputHandlers.suggestionFocusChanged |
|
257 }); |
|
258 }, |
|
259 |
|
260 /* |
|
261 * internal convenience function: old jquery plugin accepted to be fed |
|
262 * with a list of couples (value, label). The new (jquery-ui) autocomplete |
|
263 * plugin expects a list of objects with "value" and "label" properties. |
|
264 * |
|
265 * This function converts the old format to the new one. |
|
266 */ |
|
267 checkSuggestionsDataFormat: function(instanceData, suggestions) { |
|
268 // check for old (value, label) format |
|
269 if ($.isArray(suggestions) && suggestions.length && |
|
270 $.isArray(suggestions[0])){ |
|
271 if (suggestions[0].length == 2) { |
|
272 cw.log('[3.10] autocomplete init func should return {label,value} dicts instead of lists'); |
|
273 suggestions = $.map(suggestions, function(sugg) { |
|
274 return {value: sugg[0], label: sugg[1]}; |
|
275 }); |
|
276 } else { |
|
277 if(suggestions[0].length == 1){ |
|
278 suggestions = $.map(suggestions, function(sugg) { |
|
279 return {value: sugg[0], label: sugg[0]}; |
|
280 }); |
|
281 } |
|
282 } |
|
283 } |
|
284 var hiHandlers = methods.hiddenInputHandlers; |
|
285 if (suggestions.length && hiHandlers.needsHiddenInput(suggestions) |
|
286 && !instanceData.hiddenInput) { |
|
287 hiHandlers.initializeHiddenInput(instanceData); |
|
288 hiHandlers.fixUserInputInitialValue(instanceData, suggestions); |
|
289 } |
|
290 // otherwise, assume data shape is correct |
|
291 return suggestions; |
|
292 }, |
|
293 |
|
294 fixUserInputInitialValue: function(instanceData, suggestions) { |
|
295 // called when the data is loaded to reset the correct displayed |
|
296 // value in the visible input field (typically replacing an eid |
|
297 // by a displayable value) |
|
298 var curvalue = instanceData.userInput.value; |
|
299 if (!curvalue) { |
|
300 return; |
|
301 } |
|
302 for (var i=0, length=suggestions.length; i < length; i++) { |
|
303 var sugg = suggestions[i]; |
|
304 if (sugg.value == curvalue) { |
|
305 instanceData.userInput.value = sugg.label; |
|
306 return; |
|
307 } |
|
308 } |
|
309 } |
|
310 } |
|
311 }; |
|
312 |
|
313 $.fn.cwautocomplete = function(data, options) { |
|
314 return methods.__init__.apply(this, [data, options]); |
|
315 }; |
|
316 })(jQuery); |
|
317 |
|
318 |
|
319 Widgets.SuggestField = defclass('SuggestField', null, { |
|
320 __init__: function(node, options) { |
|
321 options = options || {}; |
|
322 var multi = node.getAttribute('cubicweb:multi'); |
|
323 options.multiple = (multi == "yes") ? true: false; |
|
324 var d = loadRemote(node.getAttribute('cubicweb:dataurl')); |
|
325 d.addCallback(function(data) { |
|
326 $(node).cwautocomplete(data, options); |
|
327 }); |
|
328 } |
|
329 }); |
|
330 |
|
331 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], { |
|
332 |
|
333 __init__: function(node) { |
|
334 Widgets.SuggestField.__init__(this, node, { |
|
335 method: 'get' // XXX |
|
336 }); |
|
337 } |
|
338 |
|
339 }); |
|
340 |
|
341 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], { |
|
342 __init__: function(node) { |
|
343 Widgets.SuggestField.__init__(this, node, { |
|
344 mustMatch: true |
|
345 }); |
|
346 } |
|
347 }); |
|
348 |
|
349 //remote version of RestrictedSuggestField |
|
350 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], { |
|
351 __init__: function(node, options) { |
|
352 var self = this; |
|
353 options = options || {}; |
|
354 options.delay = 50; |
|
355 // multiple selection not supported yet (still need to formalize correctly |
|
356 // initial values / display values) |
|
357 var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null'); |
|
358 if (!initialvalue) { |
|
359 initialvalue = node.value; |
|
360 } |
|
361 options.initialvalue = initialvalue; |
|
362 Widgets.SuggestField.__init__(this, node, options); |
|
363 } |
|
364 }); |
|
365 |
|
366 /** |
|
367 * .. function:: toggleTree(event) |
|
368 * |
|
369 * called when the use clicks on a tree node |
|
370 * - if the node has a `cubicweb:loadurl` attribute, replace the content of the node |
|
371 * by the url's content. |
|
372 * - else, there's nothing to do, let the jquery plugin handle it. |
|
373 */ |
|
374 function toggleTree(event) { |
|
375 var linode = jQuery(this); |
|
376 var url = linode.attr('cubicweb:loadurl'); |
|
377 if (url) { |
|
378 linode.find('ul.placeholder').remove(); |
|
379 var d = linode.loadxhtml(url, null, 'post', 'append'); |
|
380 d.addCallback(function(domnode) { |
|
381 linode.removeAttr('cubicweb:loadurl'); |
|
382 linode.find('> ul.treeview').treeview({ |
|
383 toggle: toggleTree, |
|
384 prerendered: true |
|
385 }); |
|
386 return null; |
|
387 } |
|
388 ); |
|
389 } |
|
390 } |
|
391 |
|
392 Widgets.TemplateTextField = defclass("TemplateTextField", null, { |
|
393 |
|
394 __init__: function(wdgnode) { |
|
395 this.variables = jQuery(wdgnode).attr('cubicweb:variables').split(','); |
|
396 this.options = { |
|
397 name: wdgnode.getAttribute('cubicweb:inputid'), |
|
398 rows: wdgnode.getAttribute('cubicweb:rows') || 40, |
|
399 cols: wdgnode.getAttribute('cubicweb:cols') || 80 |
|
400 }; |
|
401 // this.variableRegexp = /%\((\w+)\)s/; |
|
402 this.errorField = DIV({ |
|
403 'class': "errorMessage" |
|
404 }); |
|
405 this.textField = TEXTAREA(this.options); |
|
406 jQuery(this.textField).bind('keyup', { |
|
407 'self': this |
|
408 }, |
|
409 this.highlightInvalidVariables); |
|
410 jQuery('#substitutions').prepend(this.errorField); |
|
411 jQuery('#substitutions .errorMessage').hide(); |
|
412 wdgnode.appendChild(this.textField); |
|
413 }, |
|
414 |
|
415 /* signal callbacks */ |
|
416 |
|
417 highlightInvalidVariables: function(event) { |
|
418 var self = event.data.self; |
|
419 var text = self.textField.value; |
|
420 var unknownVariables = []; |
|
421 var it = 0; |
|
422 var group = null; |
|
423 var variableRegexp = /%\((\w+)\)s/g; |
|
424 // emulates rgx.findAll() |
|
425 while ( (group = variableRegexp.exec(text)) ) { |
|
426 if ($.inArray(group[1], self.variables) == -1) { |
|
427 unknownVariables.push(group[1]); |
|
428 } |
|
429 it++; |
|
430 if (it > 5) { |
|
431 break; |
|
432 } |
|
433 } |
|
434 var errText = ''; |
|
435 if (unknownVariables.length) { |
|
436 errText = "Detected invalid variables : " + unknownVariables.join(', '); |
|
437 jQuery('#substitutions .errorMessage').show(); |
|
438 } else { |
|
439 jQuery('#substitutions .errorMessage').hide(); |
|
440 } |
|
441 self.errorField.innerHTML = errText; |
|
442 } |
|
443 |
|
444 }); |
|
445 |
|
446 cw.widgets = { |
|
447 /** |
|
448 * .. function:: insertText(text, areaId) |
|
449 * |
|
450 * inspects textarea with id `areaId` and replaces the current selected text |
|
451 * with `text`. Cursor is then set at the end of the inserted text. |
|
452 */ |
|
453 insertText: function (text, areaId) { |
|
454 var textarea = jQuery('#' + areaId); |
|
455 if (document.selection) { // IE |
|
456 var selLength; |
|
457 textarea.focus(); |
|
458 var sel = document.selection.createRange(); |
|
459 selLength = sel.text.length; |
|
460 sel.text = text; |
|
461 sel.moveStart('character', selLength - text.length); |
|
462 sel.select(); |
|
463 } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla |
|
464 var startPos = textarea.selectionStart; |
|
465 var endPos = textarea.selectionEnd; |
|
466 // insert text so that it replaces the [startPos, endPos] part |
|
467 textarea.value = textarea.value.substring(0, startPos) + text + textarea.value.substring(endPos, textarea.value.length); |
|
468 // set cursor pos at the end of the inserted text |
|
469 textarea.selectionStart = textarea.selectionEnd = startPos + text.length; |
|
470 textarea.focus(); |
|
471 } else { // safety belt for other browsers |
|
472 textarea.value += text; |
|
473 } |
|
474 } |
|
475 }; |
|
476 |
|
477 |
|
478 // InOutWidget This contains specific InOutnWidget javascript |
|
479 // IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets) |
|
480 |
|
481 (function ($) { |
|
482 |
|
483 var methods = { |
|
484 __init__: function(fromSelect, toSelect) { |
|
485 // closed over state |
|
486 var state = {'$fromNode' : $(cw.escape('#' + fromSelect)), |
|
487 '$toNode' : $(cw.escape('#' + toSelect)), |
|
488 'name' : this.attr('id')}; |
|
489 |
|
490 function sortoptions($optionlist) { |
|
491 var $sorted = $optionlist.find('option').sort(function(opt1, opt2) { |
|
492 return $(opt1).text() > $(opt2).text() ? 1 : -1; |
|
493 }); |
|
494 // this somehow translates to an inplace sort |
|
495 $optionlist.append($sorted); |
|
496 }; |
|
497 sortoptions(state.$fromNode); |
|
498 sortoptions(state.$toNode); |
|
499 |
|
500 // will move selected options from one list to the other |
|
501 // and call an option handler on each option |
|
502 function moveoptions ($fromlist, $tolist, opthandler) { |
|
503 $fromlist.find('option:selected').each(function(index, option) { |
|
504 var $option = $(option); |
|
505 // add a new option to the target list |
|
506 $tolist.append(OPTION({'value' : $option.val()}, |
|
507 $option.text())); |
|
508 // process callback on the option |
|
509 opthandler.call(null, $option); |
|
510 // remove option from the source list |
|
511 $option.remove(); |
|
512 }); |
|
513 // re-sort both lists |
|
514 sortoptions($fromlist); |
|
515 sortoptions($tolist); |
|
516 }; |
|
517 |
|
518 function addvalues () { |
|
519 moveoptions(state.$fromNode, state.$toNode, function ($option) { |
|
520 // add an hidden input for the edit controller |
|
521 var hiddenInput = INPUT({ |
|
522 type: 'hidden', name: state.name, |
|
523 value : $option.val() |
|
524 }); |
|
525 state.$toNode.parent().append(hiddenInput); |
|
526 }); |
|
527 }; |
|
528 |
|
529 function removevalues () { |
|
530 moveoptions(state.$toNode, state.$fromNode, function($option) { |
|
531 // remove hidden inputs for the edit controller |
|
532 var selector = 'input[name=' + cw.escape(state.name) + ']' |
|
533 state.$toNode.parent().find(selector).each(function(index, input) { |
|
534 if ($(input).val() == $option.val()) { |
|
535 $(input).remove(); |
|
536 } |
|
537 }); |
|
538 }); |
|
539 }; |
|
540 |
|
541 var $this = $(this); |
|
542 $this.find('.cwinoutadd').bind( // 'add >>>' symbol |
|
543 'click', {'state' : state}, addvalues); |
|
544 $this.find('.cwinoutremove').bind( // 'remove <<<' symbol |
|
545 'click', {'state' : state}, removevalues); |
|
546 |
|
547 state.$fromNode.bind('dblclick', {'state': state}, addvalues); |
|
548 state.$toNode.bind('dblclick', {'state': state}, removevalues); |
|
549 |
|
550 } |
|
551 }; |
|
552 $.fn.cwinoutwidget = function(fromSelect, toSelect) { |
|
553 return methods.__init__.apply(this, [fromSelect, toSelect]); |
|
554 }; |
|
555 })(jQuery); |
|