PageSourceSearch

https://empop.online/bundles/empopclient/js/vendor/vxJS/widgets/xhrform.js

js empop.online collected 2026-09-25 20:48:01 UTC 14,868 bytes, 714 lines download raw bytes

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.