1/** 2 * xhrForm 3 * 4 * submits form values via XHR 5 * responses can trigger a "normal" submit, a redirect or 6 * will contain objects with name of elements, new values and 7 * possible error messages 8 * 9 * @version 0.8.1 2016-10-20 10 * @author Gregor Kofler, [email protected] 11 * 12 * @param {Object} form element 13 * @param {Object} XHR configuration object 14 * @param {Object} additional configuration settings 15 * 16 * @todo improve enableSubmit(), disableSubmit() 17 * 18 * served events: "ifuResponse", "beforeResponseCheck", "check", "beforeSubmit", "apcUpdate", "apcFinish" (only relevant when using APC in a PHP environment) 19 */ 20 21vxJS.widget.xhrForm = function(form, xhrReq, config) { 22 23 "use strict"; 24 25 if (!form.nodeName || form.nodeName.toLowerCase() != "form") { 26 throw new Error("widget.xhrForm: no form element specified!"); 27 } 28 if(!config) { 29 config = {}; 30 } 31 32 var prevErr = [], msgBoxes = [], that = {}, payload, 33 apcHidden, apcProgressBar, apcPercentage, apcPollTimeout, immediateSubmit, 34 submittedValues, submittingElement, ifrm, submittedByApp, submittingNow, 35 xhr = vxJS.xhr(xhrReq || {}), lastXhrResponse, throbberSize, 36 throbber = function() { 37 var i = "div".setProp("class", "vxJS_xhrThrobber").create(); 38 i.style.position = "absolute"; 39 vxJS.dom.setElementPosition(i, { x: -100, y: -100 } ); 40 document.body.appendChild(i); 41 return i; 42 }(); 43 44 var posThrobber = function() { 45 var p, s; 46 47 if(!submittingElement) { 48 return; 49 } 50 51 if(!throbberSize) { 52 53 // ensure that the dimensions can be calculated 54 55 throbber.style.display = "block"; 56 throbberSize = vxJS.dom.getElementSize(throbber); 57 throbber.style.display = ""; 58 } 59 60 p = vxJS.dom.getElementOffset(submittingElement); 61 s = vxJS.dom.getElementSize(submittingElement); 62 63 // parse element attribute to customize throbber position 64 65 switch(submittingElement.getAttribute("data-throbber-position")) { 66 67 case "outside-left": 68 p.x -= throbberSize.x; 69 break; 70 71 case "inside-left": 72 p.x += throbberSize.x; 73 break; 74 75 case "inside-right": 76 p.x += s.x - throbberSize.x; 77 break; 78 79 default: 80 p.x += s.x; 81 } 82 83 p.y += (s.y-throbberSize.y)/2; 84 vxJS.dom.setElementPosition(throbber, p); 85 }; 86 87 var disableSubmit = function() { 88 submittingNow = true; 89 }; 90 91 var enableSubmit = function() { 92 submittingNow = false; 93 }; 94 95 var ifuLoaded = function() { 96 var response; 97 98 if(submittedByApp) { 99 try { response = JSON.parse((ifrm.contentDocument || ifrm.contentWindow.document).body.innerHTML); } 100 catch(e) { response = {}; } 101 102 if(apcPollTimeout) { 103 window.clearTimeout(apcPollTimeout); 104 hideApcInfo(); 105 } 106 107 vxJS.dom.removeClassName(throbber, "active"); 108 enableSubmit(); 109 110 vxJS.event.serve(that, "ifuResponse", response); 111 112 handleXhrResponse(response); 113 } 114 }; 115 116 var prepareIfu = function() { 117 var action = form.action, div, s, name = "ifu_" + new Date().getTime(); 118 119 if(ifrm) { return; } 120 121 ifrm = "iframe".setProp([["name", name], ["src", "javascript:false;"]]).create(); 122 div = "div".create(ifrm); 123 s = div.style; 124 s.visibility = "hidden"; 125 s.overflow = "hidden"; 126 vxJS.dom.setElementSize(div, new Coord(0, 0)); 127 document.body.appendChild(div); 128 129 form.target = name; 130 131 if(xhrReq && xhrReq.command) { 132 form.action = action + (action.indexOf("?") == -1 ? "?" : "&") +"ifuRequest=" + xhrReq.command; 133 } 134 135 vxJS.event.addListener(ifrm, "load", ifuLoaded); 136 }; 137 138 var setValues = function(v) { 139 var val, l = v.length, e, i, j; 140 141 while(l--) { 142 if(!(e = form.elements[v[l].name])) { 143 continue; 144 } 145 146 val = v[l].value; 147 148 // NodeList requires an array of values; same types of elements are assumed 149 150 if(e instanceof NodeList && Array.isArray(val) && e.length === val.length) { 151 152 switch(e.item(0).type) { 153 case "radio": 154 case "checkbox": 155 for(i = 0; i < val.length; ++i) { 156 e[i].checked = !!val[i]; 157 } 158 break; 159 160 // select-multiple expects an array with option values that are selected 161 162 case 'select-multiple': 163 for(i = 0; i < val.length; ++i) { 164 if(Array.isArray(val[i])) { 165 for(j = e[i].options.length; j--;) { 166 e[i].options[j].selected = val[i].indexOf(e[i].options[j].value) !== -1; 167 } 168 } 169 } 170 break; 171 172 default: 173 for(i = 0; i < val.length; ++i) { 174 e[i].value = val[i] || ""; 175 } 176 } 177 178 } 179 180 else if(e instanceof Node) { 181 switch(e.type) { 182 case "radio": 183 case "checkbox": 184 e.checked = !!val; 185 break; 186 187 case 'select-multiple': 188 if(Array.isArray(val)) { 189 for(j = e.options.length; j--;) {
190 e.options[j].selected = val.indexOf(e.options[j].value) !== -1; 191 } 192 } 193 break; 194 195 default: 196 e.value = val || ""; 197 } 198 } 199 } 200 }; 201 202 var clearErrors = function() { 203 prevErr.forEach(function(e) { 204 var n; 205 e.elements.forEach(function(elem) { vxJS.dom.removeClassName(elem, "error"); }); 206 if((n = document.getElementById("error_" + e.name))) { 207 vxJS.dom.removeClassName(n, "error"); 208 if(e.text && n.lastChild) { 209 n.removeChild(n.lastChild); 210 } 211 } 212 }); 213 214 prevErr = []; 215 }; 216 217 var setErrors = function(err) { 218 err.forEach(function(e) { 219 var element = form.elements[e.name], n; 220 221 if(!element) { 222 e.elements = []; 223 } 224 225 // single element 226 227 else if(element.nodeName) { 228 e.elements = [element]; 229 vxJS.dom.addClassName(element, "error"); 230 } 231 232 // elements collection 233 234 else { 235 e.elements = vxJS.collectionToArray(element); 236 e.elements.forEach(function(elem, ndx) { 237 if(!e.ndx || e.ndx[ndx]) { 238 vxJS.dom.addClassName(elem, "error"); 239 } 240 }); 241 } 242 243 if((n = document.getElementById("error_" + e.name))) { 244 vxJS.dom.addClassName(n, "error"); 245 if(e.text) { 246 n.appendChild(document.createTextNode(e.text)); 247 } 248 } 249 }); 250 251 prevErr = err; 252 }; 253 254 var getValues = function(fe, submit) { 255 var i, v, j, o, vals = {}, e, name, ndx, hashRex = /^(.*?)\[(.*?)\]$/, matches, ndx, arrValue; 256 257 for (i = 0; i < fe.length; ++i) { 258 259 v = null; 260 matches = null; 261 e = fe[i]; 262 name = e.name; 263 264 if(config.namesToHashes && (matches = name.match(hashRex))) { 265 name = matches[1]; 266 ndx = matches[2]; // can also evaluate to empty string 267 v = {}; 268 } 269 270 if (e.type && !e.disabled) { 271 switch (e.type) { 272 273 case "radio": 274 case "checkbox": 275 if (e.checked) { 276 if(ndx) { 277 v[ndx] = e.value; 278 } 279 else { 280 v = e.value; 281 } 282 } 283 break; 284 285 case "select-multiple": 286 if(vxJS.dom.hasClassName(e, "vxJS_dualSelectBox_source")) { 287 continue; 288 } 289 o = e.options; 290 291 arrValue = []; 292 for (j = o.length; j--;) { 293 if (o[j].selected || vxJS.dom.hasClassName(e, "vxJS_dualSelectBox_dest")) { 294 arrValue.push(o[j].value); 295 } 296 } 297 if(ndx) { 298 v[ndx] = arrValue; 299 } 300 else { 301 v = arrValue; 302 } 303 break; 304 305 case "select-one": 306 if(ndx) { 307 v[ndx] = e.options[e.selectedIndex].value; 308 } 309 else { 310 v = e.options[e.selectedIndex].value; 311 } 312 break; 313 314 case "submit": 315 case "image": 316 case "button": 317 if (submit && e === submit) { 318 v = e.value; 319 } 320 break; 321 322 default: 323 if(matches) { 324 v[ndx] = e.value; 325 } 326 else { 327 v = e.value; 328 } 329 } 330 331 // continue, when no value was detected 332 333 if (v === null || (typeof v === "object" && !Object.keys(v).length)) { 334 continue; 335 } 336 337 // element name not previously found 338 339 if(typeof vals[name] === "undefined") { 340 341 // when has was detected, store it as array 342 343 vals[name] = matches ? [v] : v; 344 345 } 346 347 // found same name without hash 348 349 else if(!ndx) { 350 if(!Array.isArray(vals[name])) { 351 vals[name] = [vals[name]]; 352 } 353 vals[name].push(v); 354 } 355 356 // found same name with hash 357 358 else { 359 vals[name][ndx] = v[ndx]; 360 } 361 } 362 } 363 364 return vals; 365 }; 366 367 var clearMsgBoxes = function() { 368 msgBoxes.forEach(function(b) {vxJS.dom.deleteChildNodes(b.container);}); 369 }; 370 371 var findMsgBox = function(id) { 372 for(var i = msgBoxes.length; i--;) { 373 if (id === msgBoxes[i].id) { 374 return msgBoxes[i].container; 375 } 376 } 377 }; 378 379 // APC functionality 380 381 var hideApcInfo = function(){ 382 if(!apcPercentage && !apcProgressBar) { 383 return; 384 } 385 if(apcPercentage) { apcPercentage.style.display = "none"; } 386 if(apcProgressBar) { apcProgressBar.style.display = "none"; } 387 }; 388 389 var showApcInfo = function() { 390 if(!apcPercentage && !apcProgressBar) { 391 return; 392 } 393 if(apcPercentage) { apcPercentage.style.display = ""; } 394 if(apcProgressBar) { apcProgressBar.style.display = ""; } 395 }; 396 397 var updateApcInfo = function(r) { 398 if(!apcPercentage && !apcProgressBar) { 399 return; 400 } 401 402 var p = (r.total && r.current) ? (r.current/r.total * 100).toFixed() : 0, 403 f = r.filename; 404 405 if(apcPercentage) { 406 apcPercentage.firstChild.nodeValue = p + "%"; 407 } 408 if(apcProgressBar) { 409 apcProgressBar.firstChild.style.width = p + "%"; 410 } 411 }; 412 413 var apcPoll = function() { 414 var xhr; 415 416 showApcInfo(); 417 418 var parseApc = function() { 419 var r = this.response; 420 421 if(!r || r.cancel_upload) { 422 window.clearTimeout(apcPollTimeout); 423 vxJS.event.serve(that, "apcFinish"); 424 hideApcInfo(); 425 } 426 else { 427 vxJS.event.serve(that, "apcUpdate"); 428 updateApcInfo(r); 429 } 430 }; 431 432 (function pollCallback() { 433 if(!xhr) { 434 xhr = vxJS.xhr({ command: "apcPoll" }, { id: apcHidden.value }); 435 vxJS.event.addListener(xhr, "complete", parseApc); 436 } 437 else { 438 xhr.use(); 439 } 440 apcPollTimeout = window.setTimeout(pollCallback, 1000); 441 })(); 442 }; 443 444 /** 445 * wrap form.submit() 446 * place throbber, disable submit buttons, enable APC polling 447 */ 448 var submit = function() {
449 if(apcHidden) { 450 apcPoll(); 451 } 452 posThrobber(); 453 vxJS.dom.addClassName(throbber, "active"); 454 submittedByApp = true; 455 disableSubmit(); 456 form.submit(); 457 }; 458 459 /** 460 * handle response 461 * 462 * can handle commands (redirect, submit), 463 * set values of elements and corresponding errors 464 * fill message boxes, serve events before and after parsing response 465 */ 466 var handleXhrResponse = function(response) { 467 var l, elem, v = [], e = [], m, c, cmd, r, ndx = null; 468 469 if(!response) { 470 response = this.response; 471 } 472 473 vxJS.event.serve(that, "beforeResponseCheck", response); 474 475 clearMsgBoxes(); 476 clearErrors(); 477 enableSubmit(); 478 479 if((r = response.response && response.echo ? response.response : response)) { 480 481 if((cmd = r.command)) { 482 if(cmd === "redirect" && r.location) { 483 window.location.href = r.location; 484 return; 485 } 486 if(cmd === "submit") { 487 submit(); 488 return; 489 } 490 } 491 492 if(r.elements) { 493 l = r.elements.length; 494 while(l--) { 495 elem = r.elements[l]; 496 if(elem.value) { 497 v.push({ 498 name: elem.name, 499 value: elem.value 500 }); 501 } 502 if(elem.error) { 503 if(typeof elem.error === "object") { 504 ndx = []; 505 Object.keys(elem.error).forEach(function(k) { 506 if(elem.error[k]) { 507 ndx[k] = true; 508 } 509 }); 510 } 511 512 e.push({ 513 name: elem.name, 514 text: elem.errorText || null, 515 ndx: ndx 516 }); 517 } 518 } 519 setValues(v); 520 setErrors(e); 521 } 522 523 if((m = r.msgBoxes)) { 524 l = m.length; 525 while(l--) { 526 if(m[l].id && (c = findMsgBox(m[l].id))) { 527 c.appendChild(vxJS.dom.parse(m[l].elements)); 528 } 529 } 530 } 531 } 532 533 lastXhrResponse = response; 534 vxJS.event.serve(that, "check", response); 535 }; 536 537 var handleClick = function(e) { 538 var v; 539 540 vxJS.event.preventDefault(e); 541 542 if(submittingNow) { 543 return; 544 } 545 submittingElement = this; 546 547 vxJS.event.serve(that, "beforeSubmit"); 548 549 disableSubmit(); 550 551 if(immediateSubmit) { 552 handleXhrResponse( { command: "submit"} ); 553 } 554 else { 555 posThrobber(); 556 557 v = getValues(form.elements, this); 558 vxJS.merge(v, payload); 559 560 xhr.use(null, v, { node: throbber }).submit(); 561 562 submittedValues = v; 563 } 564 }; 565 566 that.addSubmit = function(elem) { 567 vxJS.event.addListener(elem, "click", handleClick); 568 return this; 569 }; 570 571 that.addMessageBox = function(elem, id) { 572 msgBoxes.push({ 573 container: elem, 574 id: id || "MsgBox" + msgBoxes.length 575 }); 576 return this; 577 }; 578 579 that.clearErrors = function() { 580 clearErrors(); 581 return this; 582 }; 583 584 that.getErrors = function() { 585 return prevErr; 586 }; 587 588 that.clearFileInputs = function() { 589 var inp = form.getElementsByTagName("input"), i; 590 591 if(!inp || !(i = inp.length)) { return; } 592 593 for(; --i;) { 594 if(inp[i].type == "file") { 595 inp[i].parentNode.replaceChild( 596 "input".setProp([["type", "file"], ["name", inp[i].name]]).create(), 597 inp[i]); 598 } 599 } 600 return this; 601 }; 602 603 that.forceRequest = function() { 604 var v; 605 606 vxJS.event.serve(that, "beforeSubmit"); 607 608 v = getValues(form.elements, this); 609 vxJS.merge(v, payload); 610 611 xhr.use(null, v, { node: throbber }).submit(); 612 613 submittedValues = v; 614 return this; 615 }; 616 617 that.disableImmediateSubmit = function() { 618 immediateSubmit = false; 619 return this; 620 }; 621 622 that.enableImmediateSubmit = function() { 623 immediateSubmit = true; 624 return this; 625 }; 626 627 that.enableApcUpload = function() { 628 if(typeof apcHidden === "undefined") { 629 apcHidden = function() { 630 var apc; 631 if(form.enctype !== "multipart/form-data" || !(apc = form.elements["APC_UPLOAD_PROGRESS"])) { 632 return false; 633 } 634 return apc; 635 }(); 636 637 if(apcHidden) { 638 apcProgressBar = function() { 639 var e = vxJS.dom.getElementsByClassName("apcProgressBar", form, "div")[0]; 640 if(e) { 641 vxJS.dom.cleanDOM(e); 642 return e; 643 } 644 }(); 645 646 apcPercentage = function() { 647 var e = vxJS.dom.getElementsByClassName("apcPercentage", form, "span")[0]; 648 if(e) { 649 e.appendChild(document.createTextNode("")); 650 return e; 651 } 652 }(); 653 654 hideApcInfo(); 655 } 656 } 657 return this; 658 }; 659 660 that.enableIframeUpload = function() { 661 prepareIfu(); 662 return this; 663 }; 664 665 /** 666 * allows setting of form values - "normalizes" data for internal function 667 */
668 that.setValues = function(v) { 669 var k = Object.keys(v), l = k.length, formData = []; 670 671 while(l--) { 672 formData.push({name: k[l], value: v[k[l]]}); 673 } 674 675 setValues(formData); 676 return this; 677 }; 678 679 that.getSubmittedValues = function() { 680 return submittedValues; 681 }; 682 683 that.getLastXhrResponse = function() { 684 return lastXhrResponse; 685 }; 686 687 that.setPayload = function(pl) { 688 payload = pl; 689 return this; 690 }; 691 692 that.getPayload = function() { 693 return payload; 694 }; 695 696 that.clearPayload = function() { 697 payload = null; 698 return this; 699 }; 700 701 that.isSubmittingNow = function() { 702 return submittingNow; 703 }; 704 705 vxJS.event.addListener(xhr, "complete", handleXhrResponse); 706 vxJS.event.addListener(xhr, "fail", enableSubmit); 707 vxJS.event.addListener(xhr, "timeout", enableSubmit); 708 709 that.element = form; 710 that.submit = submit; 711 that.xhr = xhr; 712 713 return that; 714};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.