|
1 /* |
|
2 * :organization: Logilab |
|
3 * :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
4 * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
5 */ |
|
6 |
|
7 CubicWeb.require('python.js'); |
|
8 CubicWeb.require('htmlhelpers.js'); |
|
9 CubicWeb.require('ajax.js'); |
|
10 |
|
11 |
|
12 //============= Eproperty form functions =====================================// |
|
13 |
|
14 /* called on Eproperty key selection: |
|
15 * - get the selected value |
|
16 * - get a widget according to the key by a sync query to the server |
|
17 * - fill associated div with the returned html |
|
18 * |
|
19 * @param varname the name of the variable as used in the original creation form |
|
20 * @param tabindex the tabindex that should be set on the widget |
|
21 */ |
|
22 function setPropValueWidget(varname, tabindex) { |
|
23 var key = firstSelected(jQuery('#pkey:'+varname)); |
|
24 if (key) { |
|
25 var args = _buildRemoteArgs('prop_widget', key, varname, tabindex); |
|
26 jQuery('#div:value:'+varname).loadxhtml(JSON_BASE_URL, args, 'post'); |
|
27 } |
|
28 } |
|
29 |
|
30 |
|
31 // *** EDITION FUNCTIONS ****************************************** // |
|
32 |
|
33 /* |
|
34 * this function is called when an AJAX form was generated to |
|
35 * make sure tabindex remains consistent |
|
36 */ |
|
37 function reorderTabindex(start) { |
|
38 var form = getNode('entityForm'); |
|
39 var inputTypes = ['INPUT', 'SELECT', 'TEXTAREA']; |
|
40 var tabindex = (start==null)?15:start; |
|
41 nodeWalkDepthFirst(form, function(elem) { |
|
42 var tagName = elem.tagName.toUpperCase(); |
|
43 if (inputTypes.contains(tagName)) { |
|
44 if (getNodeAttribute(elem, 'tabindex') != null) { |
|
45 tabindex += 1; |
|
46 elem.setAttribute('tabindex', tabindex); |
|
47 } |
|
48 return null; |
|
49 } |
|
50 return filter(isElementNode, elem.childNodes); |
|
51 }); |
|
52 } |
|
53 |
|
54 function showMatchingSelect(selectedValue, eid) { |
|
55 if (selectedValue) { |
|
56 divId = 'div' + selectedValue + '_' + eid; |
|
57 var divNode = jQuery('#' + divId); |
|
58 if (!divNode.length) { |
|
59 var args = {vid: 'unrelateddivs', relation: selectedValue, |
|
60 rql: rql_for_eid(eid), pageid: pageid, |
|
61 '__notemplate': 1}; |
|
62 jQuery.get(JSON_BASE_URL, args, function(response) { |
|
63 // append generated HTML to the cell |
|
64 jQuery('#unrelatedDivs_' + eid).append(getDomFromResponse(response)); |
|
65 _showMatchingSelect(eid, jQuery('#' + divId)); |
|
66 }); |
|
67 // deferred = doXHR(JSON_BASE_URL + queryString(args)); |
|
68 // deferred.addCallback(_buildAndShowMatchingSelect, eid, divId); |
|
69 } else { |
|
70 _showMatchingSelect(eid, divNode); |
|
71 } |
|
72 } |
|
73 else { |
|
74 _showMatchingSelect(eid, null); |
|
75 } |
|
76 } |
|
77 |
|
78 |
|
79 |
|
80 // @param divStr a HTML string returned by the server |
|
81 // function _buildAndShowMatchingSelect(eid, divId, req) { |
|
82 // var tdNode = jQuery('#unrelatedDivs_' + eid); |
|
83 // // append generated HTML to the cell |
|
84 // tdNode.appendChild(getDomFromRequest(req)); |
|
85 // _showMatchingSelect(eid, jQuery('#' + divId)); |
|
86 // } |
|
87 |
|
88 // @param divNode is a jQuery selection |
|
89 function _showMatchingSelect(eid, divNode) { |
|
90 // hide all divs, and then show the matching one |
|
91 // (would actually be better to directly hide the displayed one) |
|
92 jQuery('#unrelatedDivs_' + eid).children().hide(); |
|
93 // divNode not found means 'no relation selected' (i.e. first blank item) |
|
94 if (divNode && divNode.length) { |
|
95 divNode.show(); |
|
96 } |
|
97 } |
|
98 |
|
99 // this function builds a Handle to cancel pending insertion |
|
100 function buildPendingInsertHandle(elementId, element_name, selectNodeId, eid) { |
|
101 jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')"; |
|
102 return A({'class' : 'handle', 'href' : jscall, |
|
103 'title' : _("cancel this insert")}, '[x]'); |
|
104 } |
|
105 |
|
106 function buildEntityLine(relationName, selectedOptionNode, comboId, eid) { |
|
107 // textContent doesn't seem to work on selectedOptionNode |
|
108 var content = selectedOptionNode.firstChild.nodeValue; |
|
109 var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid); |
|
110 var link = A({'href' : 'view?rql=' + selectedOptionNode.value, |
|
111 'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id}, |
|
112 content); |
|
113 var tr = TR({'id' : 'tr' + selectedOptionNode.id}, [ TH(null, relationName), |
|
114 TD(null, [handle, link]) |
|
115 ]); |
|
116 try { |
|
117 var separator = getNode('relationSelectorRow_' + eid); |
|
118 //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least) |
|
119 // XXX Warning: separator.parentNode is not (always ?) the |
|
120 // table itself, but an intermediate node (TableSectionElement) |
|
121 var tableBody = separator.parentNode; |
|
122 tableBody.insertBefore(tr, separator); |
|
123 } catch(ex) { |
|
124 log("got exception(2)!" + ex); |
|
125 } |
|
126 } |
|
127 |
|
128 function buildEntityCell(relationName, selectedOptionNode, comboId, eid) { |
|
129 var handle = buildPendingInsertHandle(selectedOptionNode.id, 'div_insert_', comboId, eid); |
|
130 var link = A({'href' : 'view?rql=' + selectedOptionNode.value, |
|
131 'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id}, |
|
132 content); |
|
133 var div = DIV({'id' : 'div_insert_' + selectedOptionNode.id}, [handle, link]); |
|
134 try { |
|
135 var td = jQuery('#cell'+ relationName +'_'+eid); |
|
136 td.appendChild(div); |
|
137 } catch(ex) { |
|
138 alert("got exception(3)!" + ex); |
|
139 } |
|
140 } |
|
141 |
|
142 function addPendingInsert(optionNode, eid, cell, relname) { |
|
143 var value = getNodeAttribute(optionNode, 'value'); |
|
144 if (!value) { |
|
145 // occurs when the first element in the box is selected (which is not |
|
146 // an entity but the combobox title) |
|
147 return; |
|
148 } |
|
149 // 2nd special case |
|
150 if (value.indexOf('http') == 0) { |
|
151 document.location = value; |
|
152 return; |
|
153 } |
|
154 // add hidden parameter |
|
155 var entityForm = jQuery('#entityForm'); |
|
156 var oid = optionNode.id.substring(2); // option id is prefixed by "id" |
|
157 remote_exec('add_pending_insert', oid.split(':')); |
|
158 var selectNode = optionNode.parentNode; |
|
159 // remove option node |
|
160 selectNode.removeChild(optionNode); |
|
161 // add line in table |
|
162 if (cell) { |
|
163 // new relation as a cell in multiple edit |
|
164 // var relation_name = relationSelected.getAttribute('value'); |
|
165 // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_')); |
|
166 buildEntityCell(relname, optionNode, selectNode.id, eid); |
|
167 } |
|
168 else { |
|
169 var relationSelector = getNode('relationSelector_'+eid); |
|
170 var relationSelected = relationSelector.options[relationSelector.selectedIndex]; |
|
171 // new relation as a line in simple edit |
|
172 buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid); |
|
173 } |
|
174 } |
|
175 |
|
176 function cancelPendingInsert(elementId, element_name, comboId, eid) { |
|
177 // remove matching insert element |
|
178 var entityView = jqNode('a' + elementId).text(); |
|
179 jqNode(element_name + elementId).remove(); |
|
180 if (comboId) { |
|
181 // re-insert option in combobox if it was taken from there |
|
182 var selectNode = getNode(comboId); |
|
183 if (selectNode){ |
|
184 var options = selectNode.options; |
|
185 var node_id = elementId.substring(0, elementId.indexOf(':')); |
|
186 options[options.length] = OPTION({'id' : elementId, 'value' : node_id}, entityView); |
|
187 } |
|
188 } |
|
189 remote_exec('remove_pending_insert', elementId.split(':')); |
|
190 } |
|
191 |
|
192 // this function builds a Handle to cancel pending insertion |
|
193 function buildPendingDeleteHandle(elementId, eid) { |
|
194 var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');"; |
|
195 return A({'href' : jscall, 'class' : 'pendingDeleteHandle', |
|
196 'title' : _("delete this relation")}, '[x]'); |
|
197 } |
|
198 |
|
199 // @param nodeId eid_from:r_type:eid_to |
|
200 function addPendingDelete(nodeId, eid) { |
|
201 var d = async_remote_exec('add_pending_delete', nodeId.split(':')); |
|
202 d.addCallback(function () { |
|
203 // and strike entity view |
|
204 jqNode('span' + nodeId).addClass('pendingDelete'); |
|
205 // replace handle text |
|
206 jqNode('handle' + nodeId).text('+'); |
|
207 }); |
|
208 } |
|
209 |
|
210 // @param nodeId eid_from:r_type:eid_to |
|
211 function cancelPendingDelete(nodeId, eid) { |
|
212 var d = async_remote_exec('remove_pending_delete', nodeId.split(':')); |
|
213 d.addCallback(function () { |
|
214 // reset link's CSS class |
|
215 jqNode('span' + nodeId).removeClass('pendingDelete'); |
|
216 // replace handle text |
|
217 jqNode('handle' + nodeId).text('x'); |
|
218 }); |
|
219 } |
|
220 |
|
221 // @param nodeId eid_from:r_type:eid_to |
|
222 function togglePendingDelete(nodeId, eid) { |
|
223 // node found means we should cancel deletion |
|
224 if ( hasElementClass(getNode('span' + nodeId), 'pendingDelete') ) { |
|
225 cancelPendingDelete(nodeId, eid); |
|
226 } else { |
|
227 addPendingDelete(nodeId, eid); |
|
228 } |
|
229 } |
|
230 |
|
231 |
|
232 function selectForAssociation(tripletIdsString, originalEid) { |
|
233 var tripletlist = map(function (x) { return x.split(':'); }, |
|
234 tripletIdsString.split('-')); |
|
235 var d = async_remote_exec('add_pending_inserts', tripletlist); |
|
236 d.addCallback(function () { |
|
237 var args = {vid: 'edition', __mode: 'normal', |
|
238 rql: rql_for_eid(originalEid)}; |
|
239 document.location = 'view?' + as_url(args); |
|
240 }); |
|
241 |
|
242 } |
|
243 |
|
244 |
|
245 function updateInlinedEntitiesCounters(rtype) { |
|
246 jQuery('#inline' + rtype + 'slot span.icounter').each(function (i) { |
|
247 this.innerHTML = i+1; |
|
248 }); |
|
249 // var divnode = jQuery('#inline' + rtype + 'slot'); |
|
250 // var iforms = getElementsByTagAndClassName('span', 'icounter', divnode); |
|
251 // for (var i=0; i<iforms.length; i++) { |
|
252 // iforms[i].innerHTML = i+1; |
|
253 // } |
|
254 } |
|
255 |
|
256 /* |
|
257 * makes an AJAX request to get an inline-creation view's content |
|
258 * @param peid : the parent entity eid |
|
259 * @param ptype : the parent entity type |
|
260 * @param ttype : the target (inlined) entity type |
|
261 * @param rtype : the relation type between both entities |
|
262 */ |
|
263 function addInlineCreationForm(peid, ptype, ttype, rtype, role) { |
|
264 var d = async_rawremote_exec('inline_creation_form', peid, ptype, ttype, rtype, role); |
|
265 d.addCallback(function (response) { |
|
266 var linknode = getNode('add' + rtype + ':' + peid + 'link'); |
|
267 var form = jQuery(getDomFromResponse(response)); |
|
268 form.css('display', 'none'); |
|
269 form.insertBefore(linknode.parentNode).slideDown('fast'); |
|
270 // setStyle(form, {display: 'none'}); |
|
271 // insertSiblingNodesBefore(linknode.parentNode, form); |
|
272 updateInlinedEntitiesCounters(rtype); |
|
273 // slideDown(form, {'duration':0.6}); |
|
274 reorderTabindex(); |
|
275 form.trigger('inlinedform-added'); |
|
276 // MochiKit.Signal.signal(CubicWeb, 'inlinedform-added', form); |
|
277 }); |
|
278 d.addErrback(function (xxx) { |
|
279 log('xxx =', xxx); |
|
280 }); |
|
281 } |
|
282 |
|
283 /* |
|
284 * removes the part of the form used to edit an inlined entity |
|
285 */ |
|
286 function removeInlineForm(peid, rtype, eid) { |
|
287 jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() { |
|
288 $(this).remove(); |
|
289 updateInlinedEntitiesCounters(rtype); |
|
290 }); |
|
291 } |
|
292 |
|
293 /* |
|
294 * alternatively adds or removes the hidden input that make the |
|
295 * edition of the relation `rtype` possible between `peid` and `eid` |
|
296 * @param peid : the parent entity eid |
|
297 * @param rtype : the relation type between both entities |
|
298 * @param eid : the inlined entity eid |
|
299 */ |
|
300 function removeInlinedEntity(peid, rtype, eid) { |
|
301 var nodeid = ['rel', peid, rtype, eid].join('-'); |
|
302 var divid = ['div', peid, rtype, eid].join('-'); |
|
303 var noticeid = ['notice', peid, rtype, eid].join('-'); |
|
304 var node = jqNode(nodeid); |
|
305 if (node && node.length) { |
|
306 node.remove(); |
|
307 jqNode(divid).fadeTo('fast', 0.5); |
|
308 // setOpacity(divid, 0.4); |
|
309 jqNode(noticeid).fadeIn('fast'); |
|
310 // appear(jQuery('#' + noticeid), {'duration': 0.5}); |
|
311 } |
|
312 } |
|
313 |
|
314 function restoreInlinedEntity(peid, rtype, eid) { |
|
315 var nodeid = ['rel', peid, rtype, eid].join('-'); |
|
316 var divid = ['div', peid, rtype, eid].join('-'); |
|
317 var noticeid = ['notice', peid, rtype, eid].join('-'); |
|
318 var node = jqNode(nodeid); |
|
319 if (!(node && node.length)) { |
|
320 node = INPUT({type: 'hidden', id: nodeid, |
|
321 name: rtype+':'+peid, value: eid}); |
|
322 jqNode(['fs', peid, rtype, eid].join('-')).append(node); |
|
323 // appendChildNodes(fs, node); |
|
324 jqNode(divid).fadeTo('fast', 1); |
|
325 // setOpacity(divid, 1); |
|
326 jqNode(noticeid).hide(); |
|
327 // jQuery('#' + noticeid).hide(); |
|
328 } |
|
329 } |
|
330 |
|
331 function _clearPreviousErrors(formid) { |
|
332 jQuery('#' + formid + ' span.error').remove(); |
|
333 } |
|
334 |
|
335 function _displayValidationerrors(formid, eid, errors) { |
|
336 var globalerrors = []; |
|
337 var firsterrfield = null; |
|
338 for (fieldname in errors) { |
|
339 var errmsg = errors[fieldname]; |
|
340 var fieldid = fieldname + ':' + eid; |
|
341 var field = jqNode(fieldname + ':' + eid); |
|
342 if (field && getNodeAttribute(field, 'type') != 'hidden') { |
|
343 if ( !firsterrfield ) { |
|
344 firsterrfield = 'err-' + fieldid; |
|
345 } |
|
346 addElementClass(field, 'error'); |
|
347 var span = SPAN({'id': 'err-' + fieldid, 'class': "error"}, errmsg); |
|
348 field.before(span); |
|
349 } else { |
|
350 firsterrfield = formid; |
|
351 globalerrors.push(fieldname + ': ' + errmsg); |
|
352 } |
|
353 } |
|
354 if (globalerrors.length) { |
|
355 if (globalerrors.length == 1) { |
|
356 var innernode = SPAN(null, globalerrors[0]); |
|
357 } else { |
|
358 var innernode = UL(null, map(LI, globalerrors)); |
|
359 } |
|
360 // insert DIV and innernode before the form |
|
361 var div = DIV({'class' : "errorMessage"}); |
|
362 div.appendChild(innernode); |
|
363 jQuery('#' + formid).before(div); |
|
364 } |
|
365 return firsterrfield || formid; |
|
366 } |
|
367 |
|
368 |
|
369 function handleFormValidationResponse(formid, onsuccess, result) { |
|
370 // Success |
|
371 if (result[0]) { |
|
372 if (onsuccess) { |
|
373 return onsuccess(result[1]); |
|
374 } else { |
|
375 document.location.href = result[1]; |
|
376 return ; |
|
377 } |
|
378 } |
|
379 unfreezeFormButtons(formid); |
|
380 // Failures |
|
381 _clearPreviousErrors(formid); |
|
382 var descr = result[1]; |
|
383 // Unknown structure |
|
384 if ( !isArrayLike(descr) || descr.length != 2 ) { |
|
385 log('got strange error :', descr); |
|
386 updateMessage(descr); |
|
387 return ; |
|
388 } |
|
389 _displayValidationerrors(formid, descr[0], descr[1]); |
|
390 updateMessage(_("please correct errors below")); |
|
391 document.location.hash = '#header'; |
|
392 return false; |
|
393 } |
|
394 |
|
395 |
|
396 /* unfreeze form buttons when the validation process is over*/ |
|
397 function unfreezeFormButtons(formid) { |
|
398 jQuery('#progress').hide(); |
|
399 jQuery('#' + formid + ' input.validateButton').removeAttr('disabled'); |
|
400 return true; |
|
401 } |
|
402 |
|
403 /* disable form buttons while the validation is being done */ |
|
404 function freezeFormButtons(formid) { |
|
405 var formbuttons = jQuery(formid + ' input.validateButton'); |
|
406 jQuery('#progress').show(); |
|
407 jQuery(formid + ' input.validateButton').attr('disabled', 'disabled'); |
|
408 return true; |
|
409 } |
|
410 |
|
411 /* used by additional submit buttons to remember which button was clicked */ |
|
412 function postForm(bname, bvalue, formid) { |
|
413 var form = getNode(formid); |
|
414 if (bname) { |
|
415 form.appendChild(INPUT({type: 'hidden', name: bname, value: bvalue})); |
|
416 } |
|
417 var onsubmit = form.onsubmit; |
|
418 if (!onsubmit || (onsubmit && onsubmit())) { |
|
419 form.submit(); |
|
420 } |
|
421 } |
|
422 |
|
423 |
|
424 /* called on load to set target and iframeso object. |
|
425 * NOTE: this is a hack to make the XHTML compliant. |
|
426 * NOTE2: `object` nodes might be a potential replacement for iframes |
|
427 * NOTE3: there is a XHTML module allowing iframe elements but there |
|
428 * is still the problem of the form's `target` attribute |
|
429 */ |
|
430 function setFormsTarget() { |
|
431 jQuery('form.entityForm').each(function () { |
|
432 var form = jQuery(this); |
|
433 var target = form.attr('cubicweb:target'); |
|
434 if (target) { |
|
435 form.attr('target', target); |
|
436 /* do not use display: none because some browser ignore iframe |
|
437 * with no display */ |
|
438 form.append(IFRAME({name: target, id: target, |
|
439 src: 'javascript: void(0)', |
|
440 width: '0px', height: '0px'})); |
|
441 } |
|
442 }); |
|
443 } |
|
444 |
|
445 $(document).ready(setFormsTarget); |
|
446 |
|
447 function _sendForm(formid, action) { |
|
448 var zipped = formContents(formid); |
|
449 return async_remote_exec('validate_form', action, zipped[0], zipped[1]); |
|
450 } |
|
451 |
|
452 /* |
|
453 * called on traditionnal form submission : the idea is to try |
|
454 * to post the form. If the post is successful, `validateForm` redirects |
|
455 * to the appropriate URL. Otherwise, the validation errors are displayed |
|
456 * around the corresponding input fields. |
|
457 */ |
|
458 function validateForm(formid, action, onsuccess) { |
|
459 try { |
|
460 var d = _sendForm(formid, action); |
|
461 } catch (ex) { |
|
462 log('got exception', ex); |
|
463 return false; |
|
464 } |
|
465 function _callback(result, req) { |
|
466 handleFormValidationResponse(formid, onsuccess, result); |
|
467 } |
|
468 // d.addCallback(handleFormValidationResponse, formid, onsuccess); |
|
469 d.addCallback(_callback); |
|
470 return false; |
|
471 } |
|
472 |
|
473 /* |
|
474 * called by live-edit forms to submit changes |
|
475 * @param formid : the dom id of the form used |
|
476 * @param rtype : the attribute being edited |
|
477 * @param eid : the eid of the entity being edited |
|
478 * @param reload: boolean to reload page if true (when changing URL dependant data) |
|
479 */ |
|
480 function inlineValidateForm(formid, rtype, eid, divid, reload) { |
|
481 try { |
|
482 var form = getNode(formid); |
|
483 if (typeof FCKeditorAPI != "undefined") { |
|
484 for ( var name in FCKeditorAPI.__Instances ) { |
|
485 var oEditor = FCKeditorAPI.__Instances[name] ; |
|
486 if ( oEditor.GetParentForm() == form ) { |
|
487 oEditor.UpdateLinkedField(); |
|
488 } |
|
489 } |
|
490 } |
|
491 var zipped = formContents(form); |
|
492 var d = async_remote_exec('edit_field', 'apply', zipped[0], zipped[1], rtype, eid); |
|
493 } catch (ex) { |
|
494 log('got exception', ex); |
|
495 return false; |
|
496 } |
|
497 d.addCallback(function (result, req) { |
|
498 handleFormValidationResponse(formid, noop, result); |
|
499 if (reload) { |
|
500 document.location.href = result[1]; |
|
501 } else { |
|
502 var fieldview = getNode(divid); |
|
503 // XXX using innerHTML is very fragile and won't work if |
|
504 // we mix XHTML and HTML |
|
505 fieldview.innerHTML = result[2]; |
|
506 // switch inline form off only if no error |
|
507 if (result[0]) { |
|
508 // hide global error messages |
|
509 jQuery('div.errorMessage').remove(); |
|
510 jQuery('#appMsg').hide(); |
|
511 cancelInlineEdit(eid, rtype, divid); |
|
512 } |
|
513 } |
|
514 return false; |
|
515 }); |
|
516 return false; |
|
517 } |
|
518 |
|
519 /**** inline edition ****/ |
|
520 function showInlineEditionForm(eid, rtype, divid) { |
|
521 jQuery('#' + divid).hide(); |
|
522 jQuery('#' + divid + '-form').show(); |
|
523 } |
|
524 |
|
525 function cancelInlineEdit(eid, rtype, divid) { |
|
526 jQuery('#' + divid).show(); |
|
527 jQuery('#' + divid + '-form').hide(); |
|
528 } |
|
529 |
|
530 CubicWeb.provide('edition.js'); |