web/data/cubicweb.widgets.js
changeset 5658 7b9553a9db65
parent 5453 d0c8076e298b
child 5699 f4f6ee3af50b
equal deleted inserted replaced
5655:ef903fff826d 5658:7b9553a9db65
     1 /*
     1 /**
       
     2  * Functions dedicated to widgets.
       
     3  *
     2  *  :organization: Logilab
     4  *  :organization: Logilab
     3  *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     5  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     4  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     6  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     5  *
     7  *
     6  *
     8  *
     7  */
     9  */
     8 
    10 
     9 // widget namespace
    11 // widget namespace
    10 Widgets = {};
    12 Widgets = {};
    11 
    13 
    12 
    14 /**
    13 /* this function takes a DOM node defining a widget and
    15  * .. function:: buildWidget(wdgnode)
       
    16  *
       
    17  * this function takes a DOM node defining a widget and
    14  * instantiates / builds the appropriate widget class
    18  * instantiates / builds the appropriate widget class
    15  */
    19  */
    16 function buildWidget(wdgnode) {
    20 function buildWidget(wdgnode) {
    17     var wdgclass = Widgets[wdgnode.getAttribute('cubicweb:wdgtype')];
    21     var wdgclass = Widgets[wdgnode.getAttribute('cubicweb:wdgtype')];
    18     if (wdgclass) {
    22     if (wdgclass) {
    19 	var wdg = new wdgclass(wdgnode);
    23         var wdg = new wdgclass(wdgnode);
    20     }
    24     }
    21 }
    25 }
    22 
    26 
    23 /* This function is called on load and is in charge to build
    27 /**
       
    28  * .. function:: buildWidgets(root)
       
    29  *
       
    30  * This function is called on load and is in charge to build
    24  * JS widgets according to DOM nodes found in the page
    31  * JS widgets according to DOM nodes found in the page
    25  */
    32  */
    26 function buildWidgets(root) {
    33 function buildWidgets(root) {
    27     root = root || document;
    34     root = root || document;
    28     jQuery(root).find('.widget').each(function() {
    35     jQuery(root).find('.widget').each(function() {
    29 	if (this.getAttribute('cubicweb:loadtype') == 'auto') {
    36         if (this.getAttribute('cubicweb:loadtype') == 'auto') {
    30 	    buildWidget(this);
    37             buildWidget(this);
    31 	}
    38         }
    32     });
    39     });
    33 }
    40 }
    34 
       
    35 
    41 
    36 // we need to differenciate cases where initFacetBoxEvents is called
    42 // we need to differenciate cases where initFacetBoxEvents is called
    37 // with one argument or without any argument. If we use `initFacetBoxEvents`
    43 // with one argument or without any argument. If we use `initFacetBoxEvents`
    38 // as the direct callback on the jQuery.ready event, jQuery will pass some argument
    44 // as the direct callback on the jQuery.ready event, jQuery will pass some argument
    39 // of his, so we use this small anonymous function instead.
    45 // of his, so we use this small anonymous function instead.
    40 jQuery(document).ready(function() {buildWidgets();});
    46 jQuery(document).ready(function() {
    41 
    47     buildWidgets();
       
    48 });
       
    49 
       
    50 function postJSON(url, data, callback) {
       
    51     return jQuery.post(url, data, callback, 'json');
       
    52 }
       
    53 
       
    54 function getJSON(url, data, callback) {
       
    55     return jQuery.get(url, data, callback, 'json');
       
    56 }
    42 
    57 
    43 Widgets.SuggestField = defclass('SuggestField', null, {
    58 Widgets.SuggestField = defclass('SuggestField', null, {
    44     __init__: function(node, options) {
    59     __init__: function(node, options) {
    45 	var multi = node.getAttribute('cubicweb:multi') || "no";
    60         var multi = node.getAttribute('cubicweb:multi') || "no";
    46 	options = options || {};
    61         options = options || {};
    47 	options.multiple = (multi == "yes") ? true : false;
    62         options.multiple = (multi == "yes") ? true: false;
    48 	var dataurl = node.getAttribute('cubicweb:dataurl');
    63         var dataurl = node.getAttribute('cubicweb:dataurl');
    49         var method = postJSON;
    64         var method = postJSON;
    50 	if (options.method == 'get'){
    65         if (options.method == 'get') {
    51 	  method = function(url, data, callback) {
    66             method = function(url, data, callback) {
    52 	    // We can't rely on jQuery.getJSON because the server
    67                 // We can't rely on jQuery.getJSON because the server
    53 	    // might set the Content-Type's response header to 'text/plain'
    68                 // might set the Content-Type's response header to 'text/plain'
    54 	    jQuery.get(url, data, function(response) {
    69                 jQuery.get(url, data, function(response) {
    55 	      callback(evalJSON(response));
    70                     callback(cw.evalJSON(response));
    56 	    });
    71                 });
    57 	  };
    72             };
    58 	}
    73         }
    59 	var self = this; // closure
    74         var self = this; // closure
    60 	method(dataurl, null, function(data) {
    75         method(dataurl, null, function(data) {
    61 	    // in case we received a list of couple, we assume that the first
    76             // in case we received a list of couple, we assume that the first
    62 	    // element is the real value to be sent, and the second one is the
    77             // element is the real value to be sent, and the second one is the
    63 	    // value to be displayed
    78             // value to be displayed
    64 	    if (data.length && data[0].length == 2) {
    79             if (data.length && data[0].length == 2) {
    65 		options.formatItem = function(row) { return row[1]; };
    80                 options.formatItem = function(row) {
    66 		self.hideRealValue(node);
    81                     return row[1];
    67 		self.setCurrentValue(node, data);
    82                 };
    68 	    }
    83                 self.hideRealValue(node);
    69 	    jQuery(node).autocomplete(data, options);
    84                 self.setCurrentValue(node, data);
    70 	});
    85             }
       
    86             jQuery(node).autocomplete(data, options);
       
    87         });
    71     },
    88     },
    72 
    89 
    73     hideRealValue: function(node) {
    90     hideRealValue: function(node) {
    74 	var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': node.value});
    91         var hidden = INPUT({
    75 	node.parentNode.appendChild(hidden);
    92             'type': "hidden",
    76 	// remove 'name' attribute from visible input so that it is not submitted
    93             'name': node.name,
    77 	// and set correct value in the corresponding hidden field
    94             'value': node.value
    78 	jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
    95         });
    79 	    hidden.value = row[0];
    96         node.parentNode.appendChild(hidden);
    80 	});
    97         // remove 'name' attribute from visible input so that it is not submitted
       
    98         // and set correct value in the corresponding hidden field
       
    99         jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
       
   100             hidden.value = row[0];
       
   101         });
    81     },
   102     },
    82 
   103 
    83     setCurrentValue: function(node, data) {
   104     setCurrentValue: function(node, data) {
    84 	// called when the data is loaded to reset the correct displayed
   105         // called when the data is loaded to reset the correct displayed
    85 	// value in the visible input field (typically replacing an eid
   106         // value in the visible input field (typically replacing an eid
    86 	// by a displayable value)
   107         // by a displayable value)
    87 	var curvalue = node.value;
   108         var curvalue = node.value;
    88 	if (!node.value) {
   109         if (!node.value) {
    89 	    return;
   110             return;
    90 	}
   111         }
    91 	for (var i=0,length=data.length; i<length; i++) {
   112         for (var i = 0, length = data.length; i < length; i++) {
    92 	    var row = data[i];
   113             var row = data[i];
    93 	    if (row[0] == curvalue) {
   114             if (row[0] == curvalue) {
    94 		node.value = row[1];
   115                 node.value = row[1];
    95 		return;
   116                 return;
    96 	    }
   117             }
    97 	}
   118         }
    98     }
   119     }
    99 });
   120 });
   100 
   121 
   101 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], {
   122 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], {
   102 
   123 
   103     __init__ : function(node) {
   124     __init__: function(node) {
   104 	Widgets.SuggestField.__init__(this, node, {method: 'get'});
   125         Widgets.SuggestField.__init__(this, node, {
       
   126             method: 'get'
       
   127         });
   105     }
   128     }
   106 
   129 
   107 });
   130 });
   108 
   131 
   109 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
   132 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
   110 
   133 
   111     __init__ : function(node) {
   134     __init__: function(node) {
   112 	Widgets.SuggestField.__init__(this, node, {mustMatch: true});
   135         Widgets.SuggestField.__init__(this, node, {
       
   136             mustMatch: true
       
   137         });
   113     }
   138     }
   114 
   139 
   115 });
   140 });
   116 //remote version of RestrictedSuggestField
   141 //remote version of RestrictedSuggestField
   117 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
   142 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
   118     __init__: function(node, options) {
   143     __init__: function(node, options) {
   119 	var self = this;
   144         var self = this;
   120 	var multi = "no";
   145         var multi = "no";
   121 	options = options || {};
   146         options = options || {};
   122 	options.max = 50;
   147         options.max = 50;
   123 	options.delay = 50;
   148         options.delay = 50;
   124 	options.cacheLength=0;
   149         options.cacheLength = 0;
   125 	options.mustMatch = true;
   150         options.mustMatch = true;
   126         // multiple selection not supported yet (still need to formalize correctly
   151         // multiple selection not supported yet (still need to formalize correctly
   127         // initial values / display values)
   152         // initial values / display values)
   128         var initialvalue = evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
   153         var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
   129         if (!initialvalue) {
   154         if (!initialvalue) {
   130             initialvalue = node.value;
   155             initialvalue = node.value;
   131         }
   156         }
   132         options = jQuery.extend({dataType: 'json',
   157         options = jQuery.extend({
   133                                  multiple: (multi == "yes") ? true : false,
   158             dataType: 'json',
   134                                  parse: this.parseResult
   159             multiple: (multi == "yes") ? true: false,
   135                                 }, options);
   160             parse: this.parseResult
       
   161         },
       
   162         options);
   136         var dataurl = node.getAttribute('cubicweb:dataurl');
   163         var dataurl = node.getAttribute('cubicweb:dataurl');
   137         // remove 'name' from original input and add the hidden one that will
   164         // remove 'name' from original input and add the hidden one that will
   138         // store the actual value
   165         // store the actual value
   139         var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': initialvalue});
   166         var hidden = INPUT({
       
   167             'type': "hidden",
       
   168             'name': node.name,
       
   169             'value': initialvalue
       
   170         });
   140         node.parentNode.appendChild(hidden);
   171         node.parentNode.appendChild(hidden);
   141         jQuery(node).bind('result', {hinput: hidden, input:node}, self.hideRealValue)
   172         jQuery(node).bind('result', {
   142             .removeAttr('name').autocomplete(dataurl, options);
   173             hinput: hidden,
   143     },
   174             input: node
   144 
   175         },
       
   176         self.hideRealValue).removeAttr('name').autocomplete(dataurl, options);
       
   177     },
   145 
   178 
   146     hideRealValue: function(evt, data, value) {
   179     hideRealValue: function(evt, data, value) {
   147 	if (!value){
   180         if (!value) {
   148 	    value="";
   181             value = "";
   149 	}
   182         }
   150         evt.data.hinput.value = value;
   183         evt.data.hinput.value = value;
   151     },
   184     },
   152 
   185 
   153     /*
   186     /*
   154      * @param data: a list of couple (value, label) to fill the suggestion list,
   187      * @param data: a list of couple (value, label) to fill the suggestion list,
   155      *              (returned by CW through AJAX)
   188      *              (returned by CW through AJAX)
   156      */
   189      */
   157     parseResult: function(data) {
   190     parseResult: function(data) {
   158         var parsed = [];
   191         var parsed = [];
   159         for (var i=0; i < data.length; i++) {
   192         for (var i = 0; i < data.length; i++) {
   160                 var value = ''+data[i][0]; // a string is required later by jquery.autocomplete.js
   193             var value = '' + data[i][0]; // a string is required later by jquery.autocomplete.js
   161                 var label = data[i][1];
   194             var label = data[i][1];
   162                 parsed[parsed.length] = {
   195             parsed[parsed.length] = {
   163                     data: [label],
   196                 data: [label],
   164                     value: value,
   197                 value: value,
   165                     result: label
   198                 result: label
   166                 };
   199             };
   167         };
   200         };
   168         return parsed;
   201         return parsed;
   169     }
   202     }
   170 
   203 
   171 });
   204 });
   172 
   205 
   173 /*
   206 /**
       
   207  * .. class:: Widgets.SuggestForm
       
   208  *
   174  * suggestform displays a suggest field and associated validate / cancel buttons
   209  * suggestform displays a suggest field and associated validate / cancel buttons
   175  * constructor's argumemts are the same that BaseSuggestField widget
   210  * constructor's argumemts are the same that BaseSuggestField widget
   176  */
   211  */
   177 Widgets.SuggestForm = defclass("SuggestForm", null, {
   212 Widgets.SuggestForm = defclass("SuggestForm", null, {
   178 
   213 
   179     __init__ : function(inputid, initfunc, varargs, validatefunc, options) {
   214     __init__: function(inputid, initfunc, varargs, validatefunc, options) {
   180 	this.validatefunc = validatefunc || noop;
   215         this.validatefunc = validatefunc || noop;
   181 	this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc,
   216         this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc, varargs, options);
   182 						    varargs, options);
   217         this.oklabel = options.oklabel || 'ok';
   183 	this.oklabel = options.oklabel || 'ok';
   218         this.cancellabel = options.cancellabel || 'cancel';
   184 	this.cancellabel = options.cancellabel || 'cancel';
   219         bindMethods(this);
   185 	bindMethods(this);
   220         connect(this.sgfield, 'validate', this, this.entryValidated);
   186 	connect(this.sgfield, 'validate', this, this.entryValidated);
   221     },
   187     },
   222 
   188 
   223     show: function(parentnode) {
   189     show : function(parentnode) {
   224         var sgnode = this.sgfield.builddom();
   190 	var sgnode = this.sgfield.builddom();
   225         var buttons = DIV({
   191 	var buttons = DIV({'class' : "sgformbuttons"},
   226             'class': "sgformbuttons"
   192 			  [A({'href' : "javascript: noop();",
   227         },
   193 			      'onclick' : this.onValidateClicked}, this.oklabel),
   228         [A({
   194 			   ' / ',
   229             'href': "javascript: noop();",
   195 			   A({'href' : "javascript: noop();",
   230             'onclick': this.onValidateClicked
   196 			      'onclick' : this.destroy}, escapeHTML(this.cancellabel))]);
   231         },
   197 	var formnode = DIV({'class' : "sgform"}, [sgnode, buttons]);
   232         this.oklabel), ' / ', A({
   198  	appendChildNodes(parentnode, formnode);
   233             'href': "javascript: noop();",
   199 	this.sgfield.textinput.focus();
   234             'onclick': this.destroy
   200 	this.formnode = formnode;
   235         },
   201 	return formnode;
   236         escapeHTML(this.cancellabel))]);
   202     },
   237         var formnode = DIV({
   203 
   238             'class': "sgform"
   204     destroy : function() {
   239         },
   205 	signal(this, 'destroy');
   240         [sgnode, buttons]);
   206 	this.sgfield.destroy();
   241         appendChildNodes(parentnode, formnode);
   207 	removeElement(this.formnode);
   242         this.sgfield.textinput.focus();
   208     },
   243         this.formnode = formnode;
   209 
   244         return formnode;
   210     onValidateClicked : function() {
   245     },
   211 	this.validatefunc(this, this.sgfield.taglist());
   246 
       
   247     destroy: function() {
       
   248         signal(this, 'destroy');
       
   249         this.sgfield.destroy();
       
   250         removeElement(this.formnode);
       
   251     },
       
   252 
       
   253     onValidateClicked: function() {
       
   254         this.validatefunc(this, this.sgfield.taglist());
   212     },
   255     },
   213     /* just an indirection to pass the form instead of the sgfield as first parameter */
   256     /* just an indirection to pass the form instead of the sgfield as first parameter */
   214     entryValidated : function(sgfield, taglist) {
   257     entryValidated: function(sgfield, taglist) {
   215 	this.validatefunc(this, taglist);
   258         this.validatefunc(this, taglist);
   216     }
   259     }
   217 });
   260 });
   218 
   261 
   219 
   262 /**
   220 /* called when the use clicks on a tree node
   263  * .. function:: toggleTree(event)
       
   264  *
       
   265  * called when the use clicks on a tree node
   221  *  - if the node has a `cubicweb:loadurl` attribute, replace the content of the node
   266  *  - if the node has a `cubicweb:loadurl` attribute, replace the content of the node
   222  *    by the url's content.
   267  *    by the url's content.
   223  *  - else, there's nothing to do, let the jquery plugin handle it.
   268  *  - else, there's nothing to do, let the jquery plugin handle it.
   224  */
   269  */
   225 function toggleTree(event) {
   270 function toggleTree(event) {
   226     var linode = jQuery(this);
   271     var linode = jQuery(this);
   227     var url = linode.attr('cubicweb:loadurl');
   272     var url = linode.attr('cubicweb:loadurl');
   228     if (url) {
   273     if (url) {
   229         linode.find('ul.placeholder').remove();
   274         linode.find('ul.placeholder').remove();
   230 	linode.loadxhtml(url, {callback: function(domnode) {
   275         linode.loadxhtml(url, {
   231 	    linode.removeAttr('cubicweb:loadurl');
   276             callback: function(domnode) {
   232 	    jQuery(domnode).treeview({toggle: toggleTree,
   277                 linode.removeAttr('cubicweb:loadurl');
   233 				      prerendered: true});
   278                 jQuery(domnode).treeview({
   234 	    return null;
   279                     toggle: toggleTree,
   235 	}}, 'post', 'append');
   280                     prerendered: true
       
   281                 });
       
   282                 return null;
       
   283             }
       
   284         },
       
   285         'post', 'append');
   236     }
   286     }
   237 }
   287 }
   238 
   288 
   239 
   289 /**
   240 /* widget based on SIMILE's timeline widget
   290  * .. class:: Widgets.TimelineWidget
       
   291  *
       
   292  * widget based on SIMILE's timeline widget
   241  * http://code.google.com/p/simile-widgets/
   293  * http://code.google.com/p/simile-widgets/
   242  *
   294  *
   243  * Beware not to mess with SIMILE's Timeline JS namepsace !
   295  * Beware not to mess with SIMILE's Timeline JS namepsace !
   244  */
   296  */
   245 
   297 
   246 Widgets.TimelineWidget = defclass("TimelineWidget", null, {
   298 Widgets.TimelineWidget = defclass("TimelineWidget", null, {
   247     __init__: function (wdgnode) {
   299     __init__: function(wdgnode) {
   248  	var tldiv = DIV({id: "tl", style: 'height: 200px; border: 1px solid #ccc;'});
   300         var tldiv = DIV({
   249 	wdgnode.appendChild(tldiv);
   301             id: "tl",
   250 	var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
   302             style: 'height: 200px; border: 1px solid #ccc;'
   251 	var eventSource = new Timeline.DefaultEventSource();
   303         });
   252 	var bandData = {
   304         wdgnode.appendChild(tldiv);
   253 	  eventPainter:     Timeline.CubicWebEventPainter,
   305         var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
   254 	  eventSource:    eventSource,
   306         var eventSource = new Timeline.DefaultEventSource();
   255 	  width:          "100%",
   307         var bandData = {
   256 	  intervalUnit:   Timeline.DateTime[tlunit.toUpperCase()],
   308             eventPainter: Timeline.CubicWebEventPainter,
   257 	  intervalPixels: 100
   309             eventSource: eventSource,
   258 	};
   310             width: "100%",
   259 	var bandInfos = [ Timeline.createBandInfo(bandData) ];
   311             intervalUnit: Timeline.DateTime[tlunit.toUpperCase()],
   260 	var tl = Timeline.create(tldiv, bandInfos);
   312             intervalPixels: 100
   261 	var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
   313         };
   262 	Timeline.loadJSON(loadurl, function(json, url) {
   314         var bandInfos = [Timeline.createBandInfo(bandData)];
   263 			    eventSource.loadJSON(json, url); });
   315         var tl = Timeline.create(tldiv, bandInfos);
       
   316         var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
       
   317         Timeline.loadJSON(loadurl, function(json, url) {
       
   318             eventSource.loadJSON(json, url);
       
   319         });
   264 
   320 
   265     }
   321     }
   266 });
   322 });
   267 
   323 
   268 Widgets.TemplateTextField = defclass("TemplateTextField", null, {
   324 Widgets.TemplateTextField = defclass("TemplateTextField", null, {
   269 
   325 
   270     __init__ : function(wdgnode) {
   326     __init__: function(wdgnode) {
   271 	this.variables = getNodeAttribute(wdgnode, 'cubicweb:variables').split(',');
   327         this.variables = jQuery(wdgnode).attr('cubicweb:variables').split(',');
   272 	this.options = {'name'   : wdgnode.getAttribute('cubicweb:inputid'),
   328         this.options = {
   273 			'rows' : wdgnode.getAttribute('cubicweb:rows') || 40,
   329             name: wdgnode.getAttribute('cubicweb:inputid'),
   274 			'cols' : wdgnode.getAttribute('cubicweb:cols') || 80
   330             rows: wdgnode.getAttribute('cubicweb:rows') || 40,
   275 		       };
   331             cols: wdgnode.getAttribute('cubicweb:cols') || 80
   276 	// this.variableRegexp = /%\((\w+)\)s/;
   332         };
   277 	this.errorField = DIV({'class' : "errorMessage"});
   333         // this.variableRegexp = /%\((\w+)\)s/;
   278 	this.textField = TEXTAREA(this.options);
   334         this.errorField = DIV({
   279 	jQuery(this.textField).bind('keyup', {'self': this}, this.highlightInvalidVariables);
   335             'class': "errorMessage"
   280 	jQuery('#substitutions').prepend(this.errorField);
   336         });
   281 	jQuery('#substitutions .errorMessage').hide();
   337         this.textField = TEXTAREA(this.options);
   282 	wdgnode.appendChild(this.textField);
   338         jQuery(this.textField).bind('keyup', {
       
   339             'self': this
       
   340         },
       
   341         this.highlightInvalidVariables);
       
   342         jQuery('#substitutions').prepend(this.errorField);
       
   343         jQuery('#substitutions .errorMessage').hide();
       
   344         wdgnode.appendChild(this.textField);
   283     },
   345     },
   284 
   346 
   285     /* signal callbacks */
   347     /* signal callbacks */
   286 
   348 
   287     highlightInvalidVariables : function(event) {
   349     highlightInvalidVariables: function(event) {
   288 	var self = event.data.self;
   350         var self = event.data.self;
   289 	var text = self.textField.value;
   351         var text = self.textField.value;
   290 	var unknownVariables = [];
   352         var unknownVariables = [];
   291 	var it = 0;
   353         var it = 0;
   292 	var group = null;
   354         var group = null;
   293 	var variableRegexp = /%\((\w+)\)s/g;
   355         var variableRegexp = /%\((\w+)\)s/g;
   294 	// emulates rgx.findAll()
   356         // emulates rgx.findAll()
   295 	while ( group=variableRegexp.exec(text) ) {
   357         while (group = variableRegexp.exec(text)) {
   296 	    if ( !self.variables.contains(group[1]) ) {
   358             if (!self.variables.contains(group[1])) {
   297 		unknownVariables.push(group[1]);
   359                 unknownVariables.push(group[1]);
   298 	    }
   360             }
   299 	    it++;
   361             it++;
   300 	    if (it > 5) {
   362             if (it > 5) {
   301 		break;
   363                 break;
   302 	    }
   364             }
   303 	}
   365         }
   304 	var errText = '';
   366         var errText = '';
   305 	if (unknownVariables.length) {
   367         if (unknownVariables.length) {
   306 	    errText = "Detected invalid variables : " + ", ".join(unknownVariables);
   368             errText = "Detected invalid variables : " + unknownVariables.join(', ');
   307 	    jQuery('#substitutions .errorMessage').show();
   369             jQuery('#substitutions .errorMessage').show();
   308 	} else {
   370         } else {
   309 	    jQuery('#substitutions .errorMessage').hide();
   371             jQuery('#substitutions .errorMessage').hide();
   310 	}
   372         }
   311 	self.errorField.innerHTML = errText;
   373         self.errorField.innerHTML = errText;
   312     }
   374     }
   313 
   375 
   314 });
   376 });
   315 
   377 
   316 
   378 cw.widgets = {
   317 CubicWeb.provide('widgets.js');
   379     /**
       
   380      * .. function:: insertText(text, areaId)
       
   381      *
       
   382      * inspects textarea with id `areaId` and replaces the current selected text
       
   383      * with `text`. Cursor is then set at the end of the inserted text.
       
   384      */
       
   385     insertText: function (text, areaId) {
       
   386         var textarea = jQuery('#' + areaId);
       
   387         if (document.selection) { // IE
       
   388             var selLength;
       
   389             textarea.focus();
       
   390             var sel = document.selection.createRange();
       
   391             selLength = sel.text.length;
       
   392             sel.text = text;
       
   393             sel.moveStart('character', selLength - text.length);
       
   394             sel.select();
       
   395         } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla
       
   396             var startPos = textarea.selectionStart;
       
   397             var endPos = textarea.selectionEnd;
       
   398             // insert text so that it replaces the [startPos, endPos] part
       
   399             textarea.value = textarea.value.substring(0, startPos) + text + textarea.value.substring(endPos, textarea.value.length);
       
   400             // set cursor pos at the end of the inserted text
       
   401             textarea.selectionStart = textarea.selectionEnd = startPos + text.length;
       
   402             textarea.focus();
       
   403         } else { // safety belt for other browsers
       
   404             textarea.value += text;
       
   405         }
       
   406     }
       
   407 };