web/data/cubicweb.edition.js
changeset 0 b97547f5f1fa
child 977 d8bb6209edcd
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     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');