web/data/cubicweb.widgets.js
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     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);