cubicweb/web/data/cubicweb.edition.js
changeset 11057 0b59724cb3f2
parent 10932 cb217b2b3463
child 11875 011730a4af73
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 /**
       
     2  * Functions dedicated to edition.
       
     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 //============= Eproperty form functions =====================================//
       
    11 /**
       
    12  * .. function:: setPropValueWidget(varname, tabindex)
       
    13  *
       
    14  * called on CWProperty 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  * * `varname`, the name of the variable as used in the original creation form
       
    20  * * `tabindex`, the tabindex that should be set on the widget
       
    21  */
       
    22 
       
    23 function setPropValueWidget(varname, tabindex) {
       
    24     var key = firstSelected(document.getElementById('pkey-subject:' + varname));
       
    25     if (key) {
       
    26         var args = {
       
    27             fname: 'prop_widget',
       
    28             pageid: pageid,
       
    29             arg: $.map([key.value, varname, tabindex], JSON.stringify)
       
    30         };
       
    31         cw.jqNode('div:value-subject:' + varname).loadxhtml(AJAX_BASE_URL, args, 'post');
       
    32     }
       
    33 }
       
    34 
       
    35 // *** EDITION FUNCTIONS ****************************************** //
       
    36 /**
       
    37  * .. function:: reorderTabindex(start, formid)
       
    38  *
       
    39  * this function is called when an AJAX form was generated to
       
    40  * make sure tabindex remains consistent
       
    41  */
       
    42 function reorderTabindex(start, formid) {
       
    43     var form = cw.getNode(formid || 'entityForm');
       
    44     var inputTypes = ['INPUT', 'SELECT', 'TEXTAREA'];
       
    45     var tabindex = (start == null) ? 15: start;
       
    46     cw.utils.nodeWalkDepthFirst(form, function(elem) {
       
    47         var tagName = elem.tagName.toUpperCase();
       
    48         if (jQuery.inArray(tagName, inputTypes) != -1) {
       
    49             if (jQuery(elem).attr('tabindex') != null) {
       
    50                 tabindex += 1;
       
    51                 jQuery(elem).attr('tabindex', tabindex);
       
    52             }
       
    53             return null;
       
    54         }
       
    55         return jQuery.grep(elem.childNodes, isElementNode);
       
    56     });
       
    57 }
       
    58 
       
    59 function showMatchingSelect(selectedValue, eid) {
       
    60     if (selectedValue) {
       
    61         var divId = 'div' + selectedValue + '_' + eid;
       
    62         var divNode = jQuery('#' + divId);
       
    63         if (!divNode.length) {
       
    64             var args = {
       
    65                 vid: 'unrelateddivs',
       
    66                 relation: selectedValue,
       
    67                 rql: rql_for_eid(eid),
       
    68                 '__notemplate': 1
       
    69             };
       
    70             var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(BASE_URL + 'view', args, 'post', 'append');
       
    71             d.addCallback(function() {
       
    72                 _showMatchingSelect(eid, jQuery('#' + divId));
       
    73             });
       
    74         } else {
       
    75             _showMatchingSelect(eid, divNode);
       
    76         }
       
    77     } else {
       
    78         _showMatchingSelect(eid, null);
       
    79     }
       
    80 }
       
    81 
       
    82 /**
       
    83  * .. function:: _showMatchingSelect(eid, divNode)
       
    84  *
       
    85  * * `divNode`, a jQuery selection
       
    86  */
       
    87 function _showMatchingSelect(eid, divNode) {
       
    88     // hide all divs, and then show the matching one
       
    89     // (would actually be better to directly hide the displayed one)
       
    90     jQuery('#unrelatedDivs_' + eid).children().hide();
       
    91     // divNode not found means 'no relation selected' (i.e. first blank item)
       
    92     if (divNode && divNode.length) {
       
    93         divNode.show();
       
    94     }
       
    95 }
       
    96 
       
    97 /**
       
    98  * .. function:: buildPendingInsertHandle(elementId, element_name, selectNodeId, eid)
       
    99  *
       
   100  * this function builds a Handle to cancel pending insertion
       
   101  */
       
   102 function buildPendingInsertHandle(elementId, element_name, selectNodeId, eid) {
       
   103     jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
       
   104     return A({
       
   105         'class': 'handle',
       
   106         'href': jscall,
       
   107         'title': _("cancel this insert")
       
   108     },
       
   109     '[x]');
       
   110 }
       
   111 
       
   112 function buildEntityLine(relationName, selectedOptionNode, comboId, eid) {
       
   113     // textContent doesn't seem to work on selectedOptionNode
       
   114     var content = selectedOptionNode.firstChild.nodeValue;
       
   115     var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
       
   116     var link = A({
       
   117         'href': 'view?rql=' + selectedOptionNode.value,
       
   118         'class': 'editionPending',
       
   119         'id': 'a' + selectedOptionNode.id
       
   120     },
       
   121     content);
       
   122     var tr = TR({
       
   123         'id': 'tr' + selectedOptionNode.id
       
   124     },
       
   125     [TH(null, relationName), TD(null, [handle, link])]);
       
   126     try {
       
   127         var separator = cw.getNode('relationSelectorRow_' + eid);
       
   128         //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
       
   129         // XXX Warning: separator.parentNode is not (always ?) the
       
   130         // table itself, but an intermediate node (TableSectionElement)
       
   131         var tableBody = separator.parentNode;
       
   132         tableBody.insertBefore(tr, separator);
       
   133     } catch(ex) {
       
   134         log("got exception(2)!" + ex);
       
   135     }
       
   136 }
       
   137 
       
   138 function buildEntityCell(relationName, selectedOptionNode, comboId, eid) {
       
   139     var handle = buildPendingInsertHandle(selectedOptionNode.id, 'div_insert_', comboId, eid);
       
   140     var link = A({
       
   141         'href': 'view?rql=' + selectedOptionNode.value,
       
   142         'class': 'editionPending',
       
   143         'id': 'a' + selectedOptionNode.id
       
   144     },
       
   145     content);
       
   146     var div = DIV({
       
   147         'id': 'div_insert_' + selectedOptionNode.id
       
   148     },
       
   149     [handle, link]);
       
   150     try {
       
   151         var td = jQuery('#cell' + relationName + '_' + eid);
       
   152         td.appendChild(div);
       
   153     } catch(ex) {
       
   154         alert("got exception(3)!" + ex);
       
   155     }
       
   156 }
       
   157 
       
   158 function addPendingInsert(optionNode, eid, cell, relname) {
       
   159     var value = jQuery(optionNode).attr('value');
       
   160     if (!value) {
       
   161         // occurs when the first element in the box is selected (which is not
       
   162         // an entity but the combobox title)
       
   163         return;
       
   164     }
       
   165     // 2nd special case
       
   166     if (value.indexOf('http') == 0) {
       
   167         document.location = value;
       
   168         return;
       
   169     }
       
   170     // add hidden parameter
       
   171     var entityForm = jQuery('#entityForm');
       
   172     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
       
   173     loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null,
       
   174                                            [oid.split(':')]), 'POST', true);
       
   175     var selectNode = optionNode.parentNode;
       
   176     // remove option node
       
   177     selectNode.removeChild(optionNode);
       
   178     // add line in table
       
   179     if (cell) {
       
   180         // new relation as a cell in multiple edit
       
   181         // var relation_name = relationSelected.getAttribute('value');
       
   182         // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
       
   183         buildEntityCell(relname, optionNode, selectNode.id, eid);
       
   184     }
       
   185     else {
       
   186         var relationSelector = cw.getNode('relationSelector_' + eid);
       
   187         var relationSelected = relationSelector.options[relationSelector.selectedIndex];
       
   188         // new relation as a line in simple edit
       
   189         buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
       
   190     }
       
   191 }
       
   192 
       
   193 function cancelPendingInsert(elementId, element_name, comboId, eid) {
       
   194     // remove matching insert element
       
   195     var entityView = cw.jqNode('a' + elementId).text();
       
   196     cw.jqNode(element_name + elementId).remove();
       
   197     if (comboId) {
       
   198         // re-insert option in combobox if it was taken from there
       
   199         var selectNode = cw.getNode(comboId);
       
   200         // XXX what on object relation
       
   201         if (selectNode) {
       
   202             var options = selectNode.options;
       
   203             var node_id = elementId.substring(0, elementId.indexOf(':'));
       
   204             options[options.length] = OPTION({
       
   205                 'id': elementId,
       
   206                 'value': node_id
       
   207             },
       
   208             entityView);
       
   209         }
       
   210     }
       
   211     elementId = elementId.substring(2, elementId.length);
       
   212     loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_insert', null,
       
   213                                            elementId.split(':')), 'GET', true);
       
   214 }
       
   215 
       
   216 /**
       
   217  * .. function:: buildPendingDeleteHandle(elementId, eid)
       
   218  *
       
   219  * this function builds a Handle to cancel pending insertion
       
   220  */
       
   221 function buildPendingDeleteHandle(elementId, eid) {
       
   222     var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
       
   223     return A({
       
   224         'href': jscall,
       
   225         'class': 'pendingDeleteHandle',
       
   226         'title': _("delete this relation")
       
   227     },
       
   228     '[x]');
       
   229 }
       
   230 
       
   231 /**
       
   232  * .. function:: addPendingDelete(nodeId, eid)
       
   233  *
       
   234  * * `nodeId`, eid_from:r_type:eid_to
       
   235  */
       
   236 function addPendingDelete(nodeId, eid) {
       
   237     var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
       
   238     d.addCallback(function() {
       
   239         // and strike entity view
       
   240         cw.jqNode('span' + nodeId).addClass('pendingDelete');
       
   241         // replace handle text
       
   242         cw.jqNode('handle' + nodeId).text('+');
       
   243     });
       
   244 }
       
   245 
       
   246 /**
       
   247  * .. function:: cancelPendingDelete(nodeId, eid)
       
   248  *
       
   249  * * `nodeId`, eid_from:r_type:eid_to
       
   250  */
       
   251 function cancelPendingDelete(nodeId, eid) {
       
   252     var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
       
   253     d.addCallback(function() {
       
   254         // reset link's CSS class
       
   255         cw.jqNode('span' + nodeId).removeClass('pendingDelete');
       
   256         // replace handle text
       
   257         cw.jqNode('handle' + nodeId).text('x');
       
   258     });
       
   259 }
       
   260 
       
   261 /**
       
   262  * .. function:: togglePendingDelete(nodeId, eid)
       
   263  *
       
   264  * * `nodeId`, eid_from:r_type:eid_to
       
   265  */
       
   266 function togglePendingDelete(nodeId, eid) {
       
   267     // node found means we should cancel deletion
       
   268     if (jQuery(cw.getNode('span' + nodeId)).hasClass('pendingDelete')) {
       
   269         cancelPendingDelete(nodeId, eid);
       
   270     } else {
       
   271         addPendingDelete(nodeId, eid);
       
   272     }
       
   273 }
       
   274 
       
   275 function selectForAssociation(tripletIdsString, originalEid) {
       
   276     var tripletlist = $.map(tripletIdsString.split('-'),
       
   277                             function(x) { return [x.split(':')] ;});
       
   278     var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null, tripletlist));
       
   279     d.addCallback(function() {
       
   280         var args = {
       
   281             vid: 'edition',
       
   282             __mode: 'normal',
       
   283             rql: rql_for_eid(originalEid)
       
   284         };
       
   285         document.location = 'view?' + asURL(args);
       
   286     });
       
   287 
       
   288 }
       
   289 
       
   290 function updateInlinedEntitiesCounters(rtype, role) {
       
   291     jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function(i) {
       
   292         this.innerHTML = i + 1;
       
   293     });
       
   294 }
       
   295 
       
   296 /**
       
   297  * .. function:: addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore)
       
   298  *
       
   299  * makes an AJAX request to get an inline-creation view's content
       
   300  * * `peid`, the parent entity eid
       
   301  *
       
   302  * * `petype`, the parent entity type
       
   303  *
       
   304  * * `ttype`, the target (inlined) entity type
       
   305  *
       
   306  * * `rtype`, the relation type between both entities
       
   307  */
       
   308 function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
       
   309     insertBefore = insertBefore || cw.getNode('add' + rtype + ':' + peid + 'link').parentNode;
       
   310     var args = ajaxFuncArgs('inline_creation_form', null, peid, petype, ttype, rtype, role, i18nctx);
       
   311     var d = loadRemote(AJAX_BASE_URL, args);
       
   312     d.addCallback(function(response) {
       
   313         var dom = getDomFromResponse(response);
       
   314         loadAjaxHtmlHead(dom);
       
   315         var form = jQuery(dom);
       
   316         form.css('display', 'none');
       
   317         form.insertBefore(insertBefore).slideDown('fast');
       
   318         updateInlinedEntitiesCounters(rtype, role);
       
   319         reorderTabindex(null, $(insertBefore).closest('form')[0]);
       
   320         jQuery(cw).trigger('inlinedform-added', form);
       
   321         // if the inlined form contains a file input, we must force
       
   322         // the form enctype to multipart/form-data
       
   323         if (form.find('input:file').length) {
       
   324             // NOTE: IE doesn't support dynamic enctype modification, we have
       
   325             //       to set encoding too.
       
   326             form.closest('form').attr('enctype', 'multipart/form-data').attr('encoding', 'multipart/form-data');
       
   327         }
       
   328         _postAjaxLoad(dom);
       
   329     });
       
   330     d.addErrback(function(xxx) {
       
   331         cw.log('xxx =', xxx);
       
   332     });
       
   333 }
       
   334 
       
   335 /**
       
   336  * .. function:: removeInlineForm(peid, rtype, role, eid, showaddnewlink)
       
   337  *
       
   338  * removes the part of the form used to edit an inlined entity
       
   339  */
       
   340 function removeInlineForm(peid, rtype, role, eid, showaddnewlink) {
       
   341     cw.jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
       
   342             $(this).remove();
       
   343             updateInlinedEntitiesCounters(rtype, role);
       
   344     });
       
   345     if (showaddnewlink) {
       
   346         toggleVisibility(showaddnewlink);
       
   347     }
       
   348 }
       
   349 
       
   350 /**
       
   351  * .. function:: removeInlinedEntity(peid, rtype, eid)
       
   352  *
       
   353  * alternatively adds or removes the hidden input that make the
       
   354  * edition of the relation `rtype` possible between `peid` and `eid`
       
   355  * * `peid`, the parent entity eid
       
   356  *
       
   357  * * `rtype`, the relation type between both entities
       
   358  *
       
   359  * * `eid`, the inlined entity eid
       
   360  */
       
   361 function removeInlinedEntity(peid, rtype, eid) {
       
   362     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
       
   363     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
       
   364     var node = cw.jqNode(nodeid);
       
   365     if (!node.attr('cubicweb:type')) {
       
   366         node.attr('cubicweb:type', node.val());
       
   367         node.val('');
       
   368         var divid = ['div', peid, rtype, eid].join('-');
       
   369         cw.jqNode(divid).fadeTo('fast', 0.5);
       
   370         var noticeid = ['notice', peid, rtype, eid].join('-');
       
   371         cw.jqNode(noticeid).fadeIn('fast');
       
   372     }
       
   373 }
       
   374 
       
   375 function restoreInlinedEntity(peid, rtype, eid) {
       
   376     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
       
   377     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
       
   378     var node = cw.jqNode(nodeid);
       
   379     if (node.attr('cubicweb:type')) {
       
   380         node.val(node.attr('cubicweb:type'));
       
   381         node.attr('cubicweb:type', '');
       
   382         cw.jqNode(['fs', peid, rtype, eid].join('-')).append(node);
       
   383         var divid = ['div', peid, rtype, eid].join('-');
       
   384         cw.jqNode(divid).fadeTo('fast', 1);
       
   385         var noticeid = ['notice', peid, rtype, eid].join('-');
       
   386         cw.jqNode(noticeid).hide();
       
   387     }
       
   388 }
       
   389 
       
   390 function _clearPreviousErrors(formid) {
       
   391     // on some case (eg max request size exceeded, we don't know the formid
       
   392     if (formid) {
       
   393         jQuery('#' + formid + 'ErrorMessage').remove();
       
   394         jQuery('#' + formid + ' span.errorMsg').remove();
       
   395         jQuery('#' + formid + ' .error').removeClass('error');
       
   396     } else {
       
   397         jQuery('span.errorMsg').remove();
       
   398         jQuery('.error').removeClass('error');
       
   399     }
       
   400 }
       
   401 
       
   402 function _displayValidationerrors(formid, eid, errors) {
       
   403     var globalerrors = [];
       
   404     var firsterrfield = null;
       
   405     for (fieldname in errors) {
       
   406         var errmsg = errors[fieldname];
       
   407         if (!fieldname) {
       
   408             globalerrors.push(errmsg);
       
   409         } else {
       
   410             var fieldid = fieldname + ':' + eid;
       
   411             var suffixes = ['', '-subject', '-object'];
       
   412             var found = false;
       
   413             // XXX remove suffixes at some point
       
   414             for (var i = 0, length = suffixes.length; i < length; i++) {
       
   415                 var field = cw.jqNode(fieldname + suffixes[i] + ':' + eid);
       
   416                 if (field && jQuery(field).attr('type') != 'hidden') {
       
   417                     if (!firsterrfield) {
       
   418                         firsterrfield = 'err-' + fieldid;
       
   419                     }
       
   420                     jQuery(field).addClass('error');
       
   421                     var span = SPAN({
       
   422                         'id': 'err-' + fieldid,
       
   423                         'class': "errorMsg"
       
   424                     },
       
   425                     errmsg);
       
   426                     field.before(span);
       
   427                     found = true;
       
   428                     break;
       
   429                 }
       
   430             }
       
   431             if (!found) {
       
   432                 firsterrfield = formid;
       
   433                 globalerrors.push(_(fieldname) + ' : ' + errmsg);
       
   434             }
       
   435         }
       
   436     }
       
   437     if (globalerrors.length) {
       
   438        if (globalerrors.length == 1) {
       
   439            var innernode = SPAN(null, globalerrors[0]);
       
   440        } else {
       
   441            var linodes =[];
       
   442            for(var i=0; i<globalerrors.length; i++){
       
   443              linodes.push(LI(null, globalerrors[i]));
       
   444            }
       
   445            var innernode = UL(null, linodes);
       
   446        }
       
   447         // insert DIV and innernode before the form
       
   448         var div = DIV({
       
   449             'class': "errorMessage",
       
   450             'id': formid + 'ErrorMessage'
       
   451         });
       
   452         div.appendChild(innernode);
       
   453         jQuery('#' + formid).before(div);
       
   454     }
       
   455     return firsterrfield || formid;
       
   456 }
       
   457 
       
   458 function handleFormValidationResponse(formid, onsuccess, onfailure, result, cbargs) {
       
   459     // Success
       
   460     if (result[0]) {
       
   461         if (onsuccess) {
       
   462             onsuccess(result, formid, cbargs);
       
   463         } else {
       
   464             document.location.href = result[1];
       
   465         }
       
   466         return true;
       
   467     }
       
   468     if (onfailure && ! onfailure(result, formid, cbargs)) {
       
   469         return false;
       
   470     }
       
   471     unfreezeFormButtons(formid);
       
   472     // Failures
       
   473     _clearPreviousErrors(formid);
       
   474     var descr = result[1];
       
   475     var errmsg;
       
   476     // Unknown structure
       
   477     if ( !cw.utils.isArrayLike(descr) || descr.length != 2 ) {
       
   478         errmsg = descr;
       
   479     } else {
       
   480         _displayValidationerrors(formid, descr[0], descr[1]);
       
   481         errmsg = _("please correct errors below");
       
   482     }
       
   483     updateMessage(errmsg);
       
   484     // ensure the browser does not scroll down
       
   485     document.location.hash = '#header';
       
   486     return false;
       
   487 }
       
   488 
       
   489 /**
       
   490  * .. function:: unfreezeFormButtons(formid)
       
   491  *
       
   492  * unfreeze form buttons when the validation process is over
       
   493  */
       
   494 function unfreezeFormButtons(formid) {
       
   495     jQuery('#progress').hide();
       
   496     // on some case (eg max request size exceeded, we don't know the formid
       
   497     if (formid) {
       
   498         jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
       
   499     } else {
       
   500         jQuery('.validateButton').removeAttr('disabled');
       
   501     }
       
   502     return true;
       
   503 }
       
   504 
       
   505 /**
       
   506  * .. function:: freezeFormButtons(formid)
       
   507  *
       
   508  * disable form buttons while the validation is being done
       
   509  */
       
   510 function freezeFormButtons(formid) {
       
   511     jQuery('#progress').show();
       
   512     jQuery('#' + formid + ' .validateButton').attr('disabled', 'disabled');
       
   513     return true;
       
   514 }
       
   515 
       
   516 /**
       
   517  * .. function:: postForm(bname, bvalue, formid)
       
   518  *
       
   519  * used by additional submit buttons to remember which button was clicked
       
   520  */
       
   521 function postForm(bname, bvalue, formid) {
       
   522     var form = cw.getNode(formid);
       
   523     if (bname) {
       
   524         var child = form.appendChild(INPUT({
       
   525             type: 'hidden',
       
   526             name: bname,
       
   527             value: bvalue
       
   528         }));
       
   529     }
       
   530     var onsubmit = form.onsubmit;
       
   531     if (!onsubmit || (onsubmit && onsubmit())) {
       
   532         form.submit();
       
   533     }
       
   534     if (bname) {
       
   535         jQuery(child).remove();
       
   536     }
       
   537 }
       
   538 
       
   539 /**
       
   540  * Cancel the operations done on the given form.
       
   541  *
       
   542  */
       
   543 $(function () {
       
   544     $(document).on('click', '.cwjs-edition-cancel', function (evt) {
       
   545         var $mynode = $(evt.currentTarget),
       
   546             $form = $mynode.closest('form'),
       
   547             $error = $form.find(':input[name="__errorurl"]'),
       
   548             errorurl = $error.attr('value'),
       
   549             args = ajaxFuncArgs('cancel_edition', null, errorurl);
       
   550         loadRemote(AJAX_BASE_URL, args, 'POST', true);
       
   551         history.back();
       
   552         return false;
       
   553     });
       
   554 });
       
   555 
       
   556 
       
   557 /**
       
   558  * .. function:: validateForm(formid, action, onsuccess, onfailure)
       
   559  *
       
   560  * called on traditionnal form submission : the idea is to try
       
   561  * to post the form. If the post is successful, `validateForm` redirects
       
   562  * to the appropriate URL. Otherwise, the validation errors are displayed
       
   563  * around the corresponding input fields.
       
   564  */
       
   565 function validateForm(formid, action, onsuccess, onfailure) {
       
   566     freezeFormButtons(formid);
       
   567     try {
       
   568         var zipped = cw.utils.formContents(formid);
       
   569         var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
       
   570         var d = loadRemote(AJAX_BASE_URL, args, 'POST');
       
   571     } catch(ex) {
       
   572         cw.log('got exception', ex);
       
   573         return false;
       
   574     }
       
   575     d.addCallback(function(result, req) {
       
   576         handleFormValidationResponse(formid, onsuccess, onfailure, result);
       
   577     });
       
   578     return false;
       
   579 }