PageSourceSearch

https://www.autoby.jp/static/lib/js/jquery-form.js?260929023659

js autoby.jp collected 2026-10-02 06:40:27 UTC 34,937 bytes, 1,206 lines download raw bytes

1/*
2 * jQury form helper plugin
3 * 
4 * The MIT License
5 * 
6 * Copyright (c) 2012- Shoji Matsumoto ([email protected])
7 * 
8 * Permission is hereby granted, free of charge, to any person obtaining a copy
9 * of this software and associated documentation files (the "Software"), to deal
10 * in the Software without restriction, including without limitation the rights
11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 * copies of the Software, and to permit persons to whom the Software is
13 * furnished to do so, subject to the following conditions:
14 * 
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 * 
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 * THE SOFTWARE.
25 * 
26 *********************************************************************
27 * autocheck max filesize in type="file"
28 * <input type="file" data-max-size="xxxx">
29 * $(input).on('fileoversized', function(ev, oversized_files){
30 *    ...
31 * })
32 *********************************************************************
33 * 
34 * $(form).formGet()
35 * get form :input values as object (hash)
36 * 
37 * HTML:
38 * <form>
39 *   <input name="a" value="1">
40 *   <input type="checkbox" name="cb" value="cb1" checked="checked">
41 *   <input type="checkbox" name="cb" value="cb2">
42 *   <input type="checkbox" name="cb" value="cb3" checked="checked">
43 * </form>
44 * 
45 * script:
46 * $(form).fromGet()
47 *   -> { a:"1", cb:["cb1","cb3"] }
48 * 
49 **********
50 * $(form).formSet(obj)
51 * set form :input values with object (hash)
52 * $(form).formSet(obj, true) : clear fields except hidden before set
53 * $(form).formSet(obj, true, true) :  clear all fields include hidden before set
54 * 
55 * HTML:
56 * <form>
57 *   <input name="a" value="1">
58 *   <input type="checkbox" name="cb" value="cb1" checked="checked">
59 *   <input type="checkbox" name="cb" value="cb2">
60 *   <input type="checkbox" name="cb" value="cb3" checked="checked">
61 * </form>
62 * 
63 * script:
64 * $(form).formSet({
65 *   a:"2",
66 *   cb:["cb2", "cb3"]
67 * })
68 * 
69 * *********
70 * $.formPost($(form))
71 * set submit() for <form>: Ajax post and set error reasons.
72 * 
73 * HTML:
74 * <form method="post" action="path/to/json_api/to/post">
75 *   <span class="error error_@all">error placeholder for all</span>
76 *   <input name="p1"><span class="error_msg" data-for="p1">error placeholder for p1</span>
77 *   <input name="p2"><span class="error_msg" data-for="p2">error placeholder for p2</span>
78 *   <input type="submit">
79 * </form>
80 * 
81 * script:
82 * $(function(){
83 *   $("form").submit(function(){
84 *     $(this).formPost()
85 *     .then(function(){
86 *       alert('success');
87 *       location.href="success.html"
88 *     }).fail(function(res){
89 *       if (res.ajaxerror){ alert('connection error:'+res.ajaxerror.statusText) }
90 *       if (res.systemerror) { alert('serverside error:'+res.systemerror); }
91 *     });
92 *     return false;
93 *   });
94 * });
95 * 
96 * expected server response:
97 * { success:true; }
98 * or
99 * { errors:{ '<field name>':'<error message>', ... }
100 * or
101 * { systemerror:'system error message' }
102 * 
103 */
104
105(function($) {
106	
107	$(document).on('change.datamaxsize', '[type="file"][data-max-size]', function(ev){
108		var max_size = parseInt($(this).attr('data-max-size'), 10),
109			oversized = Array.from(this.files).filter(function(file){return file.size > max_size});
110		if (oversized.length) {
111			ev.preventDefault();
112			$(this).trigger('fileoversized', [oversized]);
113		}
114	});
115	
116	$.extend({
117		MAIL_RX : /^([a-zA-Z0-9])+([a-zA-Z0-9\._+-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/,
118		URL_RX : /^(https?|ftp|file):\/\/[-a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#\/%=~_|]$/,
119		DATE_RX : /^(\d{4})[-\/](1[012]|0?[1-9])[-\/](3[01]|[12][0-9]|0?[1-9])$/,
120		TIME_RX : /^(2[0-4]|[01]?[0-9]):([0-5][0-9])(?::([0-5][0-9]))?$/,
121		DATETIME_RX : /^(\d{4})[-\/](1[012]|0?[1-9])[-\/](3[01]|[12][0-9]|0?[1-9])\s+(2[0-4]|[01]?[0-9]):([0-5][0-9])(?::([0-5][0-9]))?$/
122	});
123	
124	/* support placeholder for not-supported browsers */
125	
126	var PLACEHOLDER_TARGET = ':input[placeholder]:not([type="checkbox"],[type="radio"])';
127	
128	var has_placeholder = 'placeholder' in document.createElement('input') && 'placeholder' in document.createElement('textarea');
129	
130	if (has_placeholder) {
131		$.fn.setPlaceholder = function(){ return this; };
132		$.fn.cleanPlaceholder = function(){ return this; };
133	} else {
134		(function(){
135			var org_val = $.fn.val;
136			$.fn.setPlaceholder = function(){
137				this.filter(PLACEHOLDER_TARGET).filter(':not(.placeholder)').each(function(){
138					var $this = $(this), v = org_val.call($this);
139					if (v === undefined || v === '') {
140						if ($this.attr('type') == 'password') {
141							var $dummy;
142							if ($.browser.msie && parseInt($.browser.version) < 9) {
143								var text = $this.outerHTML()
144									.replace(/type=["']?password["']?/,'type="text"');
145								$dummy = $(text);
146							} else {
147								$dummy = $(this).clone().attr('type', 'text');
148							}
149							$dummy.addClass('placeholder-dummy').removeAttr('name')
150								.on('focus', function(){
151									$(this).hide().prev().show().trigger('focus');
152									$(this).remove();
153								});
154							$this.after($dummy.setPlaceholder()).hide();
155						} else {
156							org_val.call($this, $this.attr('placeholder'));
157						}
158						$this.addClass('placeholder');
159					}
160					if ($this.is(':focus')) {
161						$this.trigger('focus');
162					}
163				});
164				return this;
165			};
166			$.fn.cleanPlaceholder = function(){
167				this.filter(PLACEHOLDER_TARGET).filter('.placeholder').removeClass('placeholder').each(function(){
168					org_val.call($(this), '');
169				});
170				return this;
171			};
172			$.fn.val = function(v){
173				if (!arguments.length) {
174					if (this.filter(PLACEHOLDER_TARGET).hasClass('placeholder')) {
175						return '';
176					}
177					return org_val.call(this);
178				}
179				if (this.hasClass('placeholder-dummy')) {
180					org_val.call(this.prev().cleanPlaceholder(), v).setPlaceholder();
181				}
182				return org_val.call(this, v).removeClass('placeholder').setPlaceholder();
183			};
184		})();
185		
186		$(document).on('submit', 'form', function(ev){
187			$(ev.currentTarget).find(PLACEHOLDER_TARGET).cleanPlaceholder();
188		}).on('reset', 'form', function(ev){
189			var $targets = $(ev.currentTarget).find(PLACEHOLDER_TARGET).cleanPlaceholder();
190			setTimeout(function(){
191				$targets.setPlaceholder();
192			});
193		});
194		
195		$(document).on('focus keyup change', PLACEHOLDER_TARGET, function(ev){
196			$(ev.currentTarget).cleanPlaceholder();
197		}).on('blur', PLACEHOLDER_TARGET, function(ev){
198			$(ev.currentTarget).setPlaceholder();
199		});
200		
201		$(document).ready(function(){
202			$(PLACEHOLDER_TARGET).setPlaceholder();
203		});
204		$(window).on('beforeunload', function(){
205			$(PLACEHOLDER_TARGET).cleanPlaceholder();
206		});
207		
208	}
209	
210	function _check_is_form(elm, funcname, throw_when_not_form){
211		funcname = funcname === undefined ? 'jquery-form.js' : funcname;
212		if (!elm) {
213			console.error(funcname + ': target is not found', elm);
214			throw funcname + ': target is not found';
215		}
216		if (elm.nodeName != 'FORM') {
217			if (throw_when_not_form) {
218				console.error(funcname + ': target is not <form>', elm);
219				throw funcname + ': target is not <form>';
220			}
221			console.warn(funcname + ': target is not <form>')
222		}
223	}
224	
225	$.fn.extend({
226		
227		formGet : function() {
228			/**
229			 * serialize form to Object.
230			 * $('form').serializeObject() returns
231			 * { <name1>:<value1_1>,
232			 *   <name2>:[ <value2_1>, <value2_2>, ...],
233			 *    :
234			 *  }
235			 */
236			_check_is_form(this[0], 'formGest');
237			/*
238			if ($(this).hasClass('locked')) {
239				'console' in window && console.error('WARN: formGet() cannot get values from fomrLock()ed form');
240			}*/
241			var is_locking = $(this).formIsLocking();
242			if (is_locking) {
243				$(this).formUnlock();
244			}
245			if (!has_placeholder){
246				$(this).filter(PLACEHOLDER_TARGET).cleanPlaceholder();
247			}
248			var ret = {}, data;
249			if (!$(this).is('form')) {
250				var $org = $('<span></span>').insertBefore($(this));
251				data = $('<form></form>').append($(this)).serializeArray();
252				$org.replaceWith($(this));
253			} else {
254				data = $(this).serializeArray();
255			}
256			$(data).each(function(i, v) {
257				if (ret[v.name] !== undefined) {
258					if (!ret[v.name].push) {
259						ret[v.name] = [ ret[v.name] ];
260					}
261					ret[v.name].push(v.value || '');
262				} else {
263					ret[v.name] = v.value;
264				}
265			});
266			if (!has_placeholder){
267				$(this).filter(PLACEHOLDER_TARGET).setPlaceholder();
268			}
269			if (is_locking) {
270				$(this).formLock();
271			}
272			return ret;
273		},
274
275		formSet : function(obj, doClear, clearFields, eventName) {
276			/**
277			 *  @param obj : json object (from formGet)
278			 *  @param doClear : clear form before set : default true
279			 *  @param clearFields : clear hidden input when clear form : default false
280			 *  @param eventName : event name for 'change' / when null, not fire any event
281			 */
282			_check_is_form(this[0], 'formSet');
283			if (doClear === undefined || doClear) {
284				this.formClear(clearFields);
285			}
286			var is_locking = this.formIsLocking();
287			this.formUnlock();
288			var form = this;
289			var changed_inputs = [];
290			$.each(obj, function(k, v) {
291				form.find('[data-form-name="' + k + '"]').each(function(){
292					if ($(this).is('img')) {
293						$(this).attr('src', v);
294					} else if ($(this).is('a')) {
295							$(this).attr('href', v);
296					} else {
297						$(this).text(v);
298					}
299				});
300				form.find(':input[name="' + k + '"]').each(function() {
301					var t = this.type, ov = null, changed = false, $this = $(this);
302					if (t === undefined) {
303						t = 'text';
304					}
305					switch (t) {
306					case 'button':
307					case 'submit':
308					case 'reset':
309						break;
310					case 'checkbox':
311						ov = this.checked;
312						function check_or_uncheck($input, v){
313							var checked = false;
314							if ($input.is('[value]')){
315								checked = $input.attr('value') == v;
316							} else {
317								checked = (v == true || v == 'on' || v == 'true');
318							}
319							$input[0].checked = checked;
320							return checked;
321						}
322						if (v instanceof Array) {
323							var cb = $(this).val(), i, l = v.length;
324							this.checked = false;
325							for (i = 0; i < l; i++) {
326								if (check_or_uncheck($this, v[i])) {
327									break;
328								}
329							}
330						} else {
331							check_or_uncheck($this, v);
332						}
333						changed = ov != this.checked;
334						break;
335					case 'radio':
336						ov = this.checked;
337						this.checked = ($this.val() == v);
338						changed = ov != this.checked;
339						break;
340					default:
341						ov = $this.val();
342						$this.val(v);
343						changed = ov != v;
344					}
345					if (changed) {
346						// $this.change();
347						changed_inputs.push($this);
348					}
349				});
350			});
351			if (!has_placeholder){
352				$(this).filter(PLACEHOLDER_TARGET).setPlaceholder();
353			}
354			if (eventName !== null) {
355				eventName = eventName || 'change';
356				$.each(changed_inputs, function(){
357					var _this = this;
358					setTimeout(function(){
359						_this.trigger(eventName);
360					}, 1);
361				});
362			}
363			
364			if (is_locking) {
365				this.formLock();
366			}
367			return this;
368		},
369
370		formClear : function(clearFields) {
371			/**
372			 * clear form inputs.
373			 * $('form').formClear();
374			 *   clear all fields without type="hidden"
375			 * $('form').formClear(true);
376			 *   clear all fields
377			 * $('form').formClear(['name1','name2',...]);
378			 *   clear 'name1', 'name2', ... fields (include hidden)
379			 */
380			_check_is_form(this[0], 'formClear');
381			/*
382			if (!has_placeholder){
383				$(this).filter(PLACEHOLDER_TARGET).cleanPlaceholder();
384			}
385			*/
386			var clearHidden = false;
387			if (clearFields) {
388				clearHidden = true;
389			}
390			if (!Array.isArray(clearFields)) {
391				clearFields = null;
392			}
393			this.find(':input').each(function() {
394				if (clearFields && $.inArray(this.name, clearFields) < 0){
395					return;
396				}
397				switch (this.type) {
398				case 'button':
399				case 'submit':
400				case 'reset':
401					break;
402				case 'checkbox':
403				case 'radio':
404					this.checked = false;
405					$(this).syncLabel();
406					break;
407				case 'hidden':
408					if (clearHidden)
409						$(this).val('');
410					break;
411				default:
412					$(this).val('');
413					break;
414				}
415			});
416			return $(this);
417		},
418		
419		formErrors : function(errors, opts) {
420			/**
421			 * <form>
422			 * 	<input name="xxx"><span class="error_msg" data-for="xxx"></span>
423			 * </form>
424			 * 
425			 * $('form').formErrors({
426			 *    xxx : 'error message'
427			 * });
428			 * 
429			 */
430			_check_is_form(this[0], 'formErrors');
431			var form = this;
432			opts = $.extend({
433				error_class : 'error_msg',
434				alert_not_detected_errors: false
435			}, opts);
436			// form.find('.' + opts.error_class + '[data-for]').text('').
436css('display', 'none');
437			form.find('.' + opts.error_class + '[data-for]').text('').removeClass('has_error').addClass('no_error').hide();
438			var not_detected_errors = [];
439			$.each(errors || {}, function(k, v) {
440				// form.find('.' + opts.error_class + '[data-for="' + k + '"]').text(v).css('display', 'block');
441				if (!form.find('.' + opts.error_class + '[data-for="' + k + '"]').text(v).addClass('has_error').removeClass('no_error').show()[0]) {
442					not_detected_errors.push(v + ' (' + key + ')');
443				};
444			});
445			if (not_detected_errors.length) {
446				alert(not_detected_errors.join('\n'));
447			}
448			return this;
449		},
450		
451		formErrorsClear : function(opts) {
452			_check_is_form(this[0], 'formErrorsClear');
453			var form = this;
454			opts = $.extend({
455				error_class : 'error_msg'
456			}, opts);
457			form.find('.' + opts.error_class + '[data-for]').text('').css('display', 'none');
458			return this;
459		},
460		
461		formLock : function(maskopts){
462			_check_is_form(this[0], 'formLock');
463			var form = this;
464			form.mask(maskopts).data('formLocked', true);
465			form.find((':input,a')).attr('disabled', 'disabled').addClass('locked');
466			return form;
467		},
468		
469		formUnlock : function(){
470			_check_is_form(this[0], 'formUnlock');
471			var form = this;
472			form.find((':input,a')).removeAttr('disabled').removeClass('locked');
473			form.mask('destroy').removeData('formLocked');
474			return form;
475		},
476		
477		formIsLocking : function(){
478			_check_is_form(this[0], 'formIsLocking');
479			var form = this;
480			return !!form.data('formLocked');
481		},
482		
483		formValidate : function(validators){
484			/**
485			 * @param validators {
486			 *   '<name>' : [ <require num>, <regexp|function(v){ return false if error }>, '<error message>', '<require message>'],
487			 *   :
488			 *   '@all' : function(data){ return { <errors> } or undefined }, // optional
489			 *   '@required_message': 'required message' // optional
490			 * }
491			 * @return { <cleaned data> } or { '_has_error':true, '<name>': '<errormessage>', ..., '@all': '<errormessage>' }
492			 */
493			validators = $.extend({}, validators);
494			var form = this;
495			var data = form.formGet();
496			var errors = {};
497			var required_message = validators['@required_message'];
498			$.each(validators, function(k, v) {
499				if (k[0] != '@') {
500					var required = v[0], validator = v[1], message = v[2];
501					required_message = v[3] || required_message || 'required';
502					if (data[k] == '') {
503						if (required > 0) {
504							errors[k] = required_message;
505							return;
506						}
507					}
508					if (required == 0 && ( data[k] === '' || data[k] === undefined )) {
509						delete data[k];
510						return;
511					}
512					if (!Array.isArray(data[k])) {
513						if ((validator.test && !validator.test(data[k])) || (validator.call && validator(data[k]) === false)) {
514							errors[k] = v[2];
515						}
516					} else {
517						$.each(data[k], function(i, d){
518							if ((validator.test && !validator.test(d)) || (validator.call && validator(d) === false)) {
519								errors[k] = v[2];
520							}
521						});
522					}
523				}
524			});
525
526			if ($.isEmptyObject(errors)) {
527				if (validators['@all']) {
528					errors = validators['@all'](data) || {};
529				}
530			}
531			return $.isEmptyObject(errors) ? data : $.extend({ '_has_error': true }, errors);
532		},
533		
534		formPost : function(opts) {
535			/**
536			 * POST form by ajax ($.Deferred).
537			 * 
538			 * When form is submitted:
539			 *   1. preprocess:
540			 *       i)   add 'posting' class to <form>
541			 *       ii)  disabled :inputs
542			 *       iii) clear '.error_msg' texts and set display:none;
543			 *   2. POST values to <url>
544			 *   3. process response.
545			 *        res.errors is exists
546			 *          i)  set error texts to data-for="<param_name>"
547			 *          ii) reject(res)
548			 *        res.success == true
549			 *          resolve(res)
550			 *   4. postproces
551			 *       i)   wait 1500ms ( to avoid muitiple post )
552			 *       ii)  enable :inputs
553			 *       iii) remove 'posting' class from <form>
554			 * 
555			 * usage)
556			 * 
557			 * <script>
558			 * $(function(){
559			 *   $(form).submit(function(){
560			 *     $(this).formPost()
561			 *     .done(function(res){
562			 *        location.href="success.html";
563			 *     }).fail(function(res){
564			 *        if (res.systemerror) { alert(res.systemerror); }
565			 *        if (res.ajaxerror) { alert(ajaxerror.responseText); }
566			 *     });
567			 *     return false;
568			 *   }).find('.error_msg[data-for]').css('display','none');
569			 * </script>
570			 * <form id="testform" method="post" action="/path/to/post.json">
571			 *    <span class="error_msg" data-for="@all"></span>
572			 *
573			 *    <input type="text" name="param1">
574			 *    <span class="error_msg" data-for="param1"></span>
575			 *    <input type="text" name="param2">
576			 *    <span class="error_msg" data-for="param2"></span>
577			 *                     :
578			 *    <input type="sbumit">
579			 * </form>
580			 * 
581			 * expected /path/to/post.json response:
582			 * 
583			 * {
584			 *    success:true, // when sucess
585			 *    errors:{ // when validation error
586			 *        '<param_name>':'<error message>', 	// validation error for param_name
587			 *        '<param_name>':'<error message>', 	// validation error for param_name
588			 *                       :
589			 *        '@all':'<error message>'				// relational error
590			 *    },
591			 *    systemerror:'<system error message>'		// system error (ex: generic exceptions)
592			 * }
593			 * 
594			 * options)
595			 *     $(form).fomrPost({
596			 *         error_class:'<error message class - defalt:error_msg>'
597			 *         alert_not_detected_errors: <bool - defult:false>,
598			 *     })
599			 * 
600			*/
601			_check_is_form(this[0], 'formPost');
602
603			var form = $(this);
604			if (!has_placeholder) {
605				$(this).filter(PLACEHOLDER_TARGET).cleanPlaceholder();
606			}
607			
608			opts = $.extend({
609				url : $(form).attr('action'),
610				error_class : 'error_msg',
611				alert_not_detected_errors: false,
612				posting_class : 'posting',
613				method: 'post',
614				validators : { // regexp checker : { '<param>' : [ /<regexp>/, '<error message>'], ..., '@all':function(data){ return <true if relational validation is ok> } }
615					'@all' : function(data) {
616						return true;
617					}
618				},
619				clean : function(params) { // check params : if return errors object, caccel and proc errors.
620					return params;
621				},
622				filter : function(res) { // filter for server respose
623					return res;
624				},
625				crossDomain : false
626			}, opts);
627
628			var url = opts.url;
629
630			if (url === undefined) {
631				alert('params.url or form[action] is nothing.');
632			}
633
634			function afterProc(func, delay) {
635				delay = delay === undefined ? 250 : delay;
636				setTimeout(function() {
637					form.removeClass(opts.posting_class);
638					form.formUnlock();
639					func();
640				}, delay);
641			}
642			
643			return $.Deferred(function(_d) {
644				var _d2 = $.Deferred();
645				try {
646					form.addClass(opts.posting_class);
647					form.formErrors();
648					var data = form.formValidate(opts.validators);
649					if (data._has_error) {
650						form.formErrors(data, {
651							error_class: opts.error_class,
652							alert_not_detected_errors: opts.alert_not_detected_errors
653						});
654						afterProc(function(){
655							_d.resolve({
656								errors : data
657							});
658						});
659						return;
660					}
661					
662					form.formLock();
663
664					data = opts.clean(data);
665
666					data[new Date().getTime()] = '';
667
668					_d2 = $.ajax({
669						'cache' : false,
670						'url' : url,
671						'type' : opts.method.toUpperCase(),
672						'data' : data,
673						'dataType' : 'json',
674						'corssDomain' : opts.crossDomain
675					}).then(function(res) {
676						res = opts.filter(res);
677						if (res.success) {
678							afterProc(function(){
679								_d.resolve(res);
680							});
681							return;
682						}
683						if (res.systemerror) {
684							afterProc(function(){
685								_d.reject(res);
686							});
687							return;
688						}
689						if (res.errors) {
690							form.formErrors(res.errors, {
691								error_class: opts.error_class,
692								alert_not_detected_errors: opts.alert_not_detected_errors
693							});
694							afterProc(function(){
695								_d.reject(res);
696							});
697						} else {
698							afterProc(function(){
699								_d.reject(res);
700							});
701						}
702					}).fail(function(xhr, textStatus, errorThrown) {
703						afterProc(function(){
704							_d.reject({
705								'ajaxerror' : xhr
706							});
707						});
708					});
709				} catch (ex) {
710					afterProc(function(){
711						_d.reject({
712							'systemerror' : ex
713						});
714					});
715					setTimeout(function() {
716						throw ex;
717					}, 500);
718				}
719				_d.abort = function() {
720					_d2.abort && _d2.abort();
721					_d.resolve('aborted');
722				};
723			}).promise();
724		},
725		
726		formClearDirty : function(data){
727			$(this).find('form').add($(this).filter('form')).each(function(){
728				var $form = $(this);
729				$form.data('dirty_org', data || $form.formGet());
730			});
731			return this;
732			// console.log('formClearDirty:' + $.toJSON($this.data('dirty_org')));
733		},
734		
735		formIsDirty : function(){
736			/* return true if any of $(form) is dirty */
737			var is_dirty = false;
738			$(this).find('form').add($(this).filter('form')).each(function(){
739				var $form = $(this);
740				// console.log('formIsDirty:\n' + $.toJSON($form.formGet()) + '\n' + $.toJSON($form.data('dirty_org')));
741				is_dirty = $.toJSON($form.formGet()) != $.toJSON($form.data('dirty_org'));
742			});
743			return is_dirty;
744		},
745		
746		formSetDirty : function(){
747			$(this).find('form').add($(this).filter('form')).each(function(){
748				var $form = $(this);
749				// console.log('formIsDirty:\n' + $.toJSON($form.formGet()) + '\n' + $.toJSON($form.data('dirty_org')));
750				$form.removeData('dirty_org');
751			});
752			return this;
753		},
754		
755		formCheckDirty : function(){
756			$(this).find('form').add($(this).filter('form')).each(function(){
757				var $form = $(this);
758				$form.find(':input').off('change.formcheckdirty').on('change.formcheckdirty', function(ev){
759					if ($form.formIsDirty()) {
760						$form.trigger('formdirty');
761					}
762				});
763			});
764			return this;
765		},
766		
767		setBusy : function(){
768			return $(this).addClass('busy').attr('disable', 'disable');
769		},
770		
771		unsetBusy : function(){
772			return $(this).removeClass('busy').attr('disable', '');
773		},
774		
775		findLabel : function(){
776			var label_id = $('label[for="' + this.attr('id') + '"]');
777			return label_id[0] ? label_id : this.closest('label');
778		},
779		
780		syncLabel : function(options){
781			var opts = $.extend({
782				className : 'checked'
783			}, this.data('syncLabel'), options);
784			this.data('syncLabel', opts);
785			var input = this;
786			if (input.attr('type') != 'radio' && input.attr('type') != 'checkbox') {
787				return this;
788			}
789			switch (input.attr('type').toLowerCase()) {
790			case 'checkbox':
791				var checked = input[0].checked;
792				input.findLabel().toggleClass(opts.className, checked).toggleClass('not' + opts.className, !checked);
793				break;
794			case 'radio':
795				input.closest('form').find(':input[name="'+input.attr('name')+'"]').each(function(){
796					var checked = this.checked;
797					$(this).findLabel().toggleClass(opts.className, checked).toggleClass('not' + opts.className, !checked);
798				});
799				break;
800			}
801			return this;
802		},
803		
804		syncLabels : function(options){
805			var opts = $.extend({
806				className : 'checked'
807			}, this.data('syncLabels'), options);
808			this.data('syncLabels', opts);
809			var form = this;
810			form.find('input[type="checkbox"],input[type="radio"]').each(function(){ $(this).syncLabel(opts); });
811			return this;
812		},
813		
814		formPreventAutoSubmit : function() {
815			$(this).find('input:not([type]),input[type="text"],input[type="url"],input[type="date"]').on('keypress', function(ev){
816				if (ev.which ==  13) {
817					ev.preventDefault();
818				}
819			});
820		}
821		
822	});
823	
824	function _blockEvent(ev){
825		ev.stopPropagation();
826		ev.preventDefault();
827		return false;
828	};
829	
830	$.extend({
831		formLocalSupported: function(){
832			return !!(window.localStorage && window.JSON && window.JSON.stringify);
833		}
834	});
835	
836	if (window.localStorage && window.JSON && window.JSON.stringify) {
837		// save / restore / delete / expire to localStorage
838		$.fn.extend({
839			formLocalSave : function(key, filter_func){
840				// returns true for success
841				var ret = false;
842				// do not save if filter_func returns false
843				filter_func = filter_func || function(v) { return v; }
844				var v = filter_func($(this).formGet());
845				if (v) {
846					// v._saved_at is marker for data from this routine.
847					v._saved_at = new Date().getTime();
848					try {
849						v = JSON.stringify(v);
850						localStorage.setItem(key, v);
851						ret = true;
852					} catch (ex) {
853						// probably quota exceeded
854						console.error('loacalStorage.setItem:error', ex);
855					}
856				}
857				return ret;
858			},
859			formLocalRestore : function(key, filter_func){
860				// do not restore if filter_func returns false
861				filter_func = filter_func || function(v) { return v; }
862				var v = $.formLocalData(key);
863				if (v) {
864					v = filter_func(v);
865					if (v) {
866						delete v._saved_at;
867						$(this).formSet(v);
868					}
869				}
870				return this;
871			},
872		});
873		$.extend({
874			formLocalHasKey: function(key){
875				return !!(localStorage.getItem(key));
876			},
877			formLocalData : function(key){
878				var v = localStorage.getItem(key);
879				if (v) {
880					v = JSON.parse(v);
881					if (v) {
882						// delete v._saved_at;
883						return v;
884					}
885				}
886				return null;
887			},
888			formLocalDelete : function(key){
889				localStorage.removeItem(key);
890				return this;
891			},
892			formLocalExpire: function(expire_date){
893				// clean all saved form data before expire_date
894				// default expire date is before 7 days.
895				expire_date = expire_date || new Date(new Date() - 7 * 24 * 60 * 60 * 1000);
896				$.each(localStorage, function(i){
897					var k = localStorage.key(i), v = localStorage.getItem(k);
898					if (v._saved_at && (v._saved_at < expire_date)) {
899						localStorage.removeItem(k);
900					}
901				});
902			}
903		});
904	}
905	
906	
907	$.extend({
908		formExportImportSupported: function(){
909			return !!(document.createElement('a').download !== undefined && window.File && window.FileReader && window.FileList && window.Blob && window.JSON);
910		}
911	});
912	
913	if ($.formExportImportSupported()) {
914		
915		$.fn.extend({
916			
917			formExport: function(form, filename, opts){
918				/**
919				 * <a class=".formexport">export</a>
920				 * $('a.formexport').formExport($('form'), 'exported', {'optional':'values', ...});
921				 * -> save file "exported-<yyMMddhhmm>.json"
922				 */
923				
924				var $link = $(this), $form = $(form);
925				
926				if (!$link.is('a')) {
927					throw 'this is not a.'
928				}
929				
930				if (!$form.is('form')) {
931					throw 'target element is not form';
932				}
933				
934				opts = $.extend({
935					filter:function(data){
936						return data;
937					},
938				}, opts);
939				
940				$link.on('click', function(){
941					
942					var data = {
943						'exported_from':location.href,
944						'exported_at':$.dateformat(new Date(), 'yyyy-MM-dd HH:mm'),
945						'browser': navigator.userAgent,
946						'formdata':$form.formGet()
947					};
948					
949					data = opts.filter(data);
950					
951					if (!data) {
952						return;
953					}
954					
955					var text = JSON.stringify(data, null, 1);
956					
957					filename += '-' + $.dateformat(new Date(), 'yyMMddHHmm') + '.json';
958					
959					
960					$(this).attr({
961						'href':'data:application/json,' + encodeURIComponent(text),
962						'download':filename
963					});
964					
965				});
966				
967			},
968			
969			// TODO: confirm import data
970			formImport: function(opts){
971				/**
972				 * $('form').formImport().then(function(res){
973				 *   // this is form
974				 *   // res is imported
975				 * }).fail(function(error){
976				 *   // this is form
977				 *   // res is error string
978				 *   
979				 *   error:
980				 *     'invalid file format'
981				 *     
982				 *   
983				 * }
983).always();
984				 */
985				
986				var $form = $(this);
987				
988				if (!$form.is('form')) {
989					throw 'target element is not form';
990				}
991				
992				var opts = $.extend({
993					filter:function(data){
994						return data;
995					}
996				}, opts);
997				
998				var _d = $.Deferred();
999				
1000				$('<input type="file" accept="application/json,.json,.txt" />').on('change', function(ev){
1001					var file = this.files && this.files[0];
1002					// file.name, file.size, file.lastModifiedDate, file.type
1003					var reader = new FileReader(file, 'utf-8');
1004					reader.onload = (function(file, reader){
1005						return function(e) {
1006							var json;
1007							try {
1008								json = JSON.parse(reader.result, function(k,v){return v;});
1009							} catch (ex) {
1010								_d.reject('failed to parse json');
1011								return;
1012							}
1013							console.log(json);
1014							
1015							if (!json.formdata || !json.exported_at || !json.exported_from) {
1016								_d.reject('invalid data format');
1017								return;
1018							}
1019							
1020							json = opts.filter.bind($form[0])(json);
1021							
1022							if (!json) {
1023								_d.reject('aborted');
1024								return;
1025							}
1026							
1027							$form.formSet(json.formdata);
1028							
1029							_d.resolve(json);
1030						};
1031					})(file, reader);
1032					reader.onerror = function(){
1033						_d.reject('failed to read file');
1034						return;
1035					};
1036					reader.readAsText(file);
1037				}).trigger('click');
1038				
1039				return _d;
1040			}
1041			
1042		});
1043		
1044	}
1045	
1046	
1047	/**************************** events ************************/
1048	$(document)
1049		.on('click', 'a.busy, :input.busy, button.busy', _blockEvent)
1050		.on('keydown', ':input.busy', _blockEvent)
1051		.on('submit', 'form.busy', _blockEvent)
1052		.on('dblclick', 'input[type="button"],input[type="submit"],input[type="reset"]', _blockEvent);
1053	
1054	/* sync input[type="radio"] and input[type="checkbox"] and labels */
1055	
1056	$(document).on('change', 'form input[type="checkbox"],form input[type="radio"]', function(ev){
1057		$(ev.currentTarget).syncLabel();
1058	});
1059	
1060	if ($.browser.msie) {
1061		if (parseInt($.browser.version) < 9) {
1062			$(document).on('click', 'label img', function(ev){
1063				var $this = $(ev.currentTarget), $label = $this.closest('label');
1064				$('#' + $label.attr('for')).add($label.find('input')).trigger('click');
1065			});
1066		}
1067		if (parseInt($.browser.version) < 7) {
1068			var _cnt = 0;
1069			$('label:not([for]) input[type="radio"], label:not([for]) input[type="checkbox"]').each(function(){
1070				var $input = $(this), id = $input.attr('id') || 'input_cbr' + (_cnt++);
1071				$input.closest('label:not([for])').attr('for', id);
1072			});
1073		}
1074		/*
1075		$(document).on('click', 'form label:not([for])', function(ev){
1076			var $label = $(ev.currentTarget), $input = $('#' + $label.attr('for')).add($label.find('input:radio,input:checkbox'));
1077			if ($input[0]) {
1078				var checked = $input[0].checked;
1079				if ($input.attr('type') == 'radio') {
1080					$input.prop('checked', true).trigger('focus').trigger('click');
1081				} else {
1082					$input.prop('checked', !checked).trigger('focus).trigger('click');
1083				}
1084			}
1085			ev.stopPropagation();
1086			ev.preventDefault();
1087		});
1088		*/
1089	}
1090	
1091	
1092	//////// auto select value in text input when focused
1093	$(document).on('focus', 'input[type="text"],input[type="password"],input[type="url"],input[type="email"],input[type="date"],input[type="time"]',function(){
1094		this.select();
1095	});
1096	
1097	//////// add 'http://' to URL input field
1098	$(document).on('blur', 'input[type="url"]',function(){
1099		var v = $(this).val();
1100		if (v && !v.match(/^(https?:)/)) {
1101			$(this).val('http://' + v);
1102		}
1103	});
1104	
1105	$.setStyle('form .error_msg.no_error { display: none! important }');
1106	
1107	/////// toggle .focused to label[for]
1108	$(document).on('focus', ':input[id]', function(){
1109		var $label = $('label[for="' + $(this).attr('id') + '"]');
1110		$label.addClass('focused');
1111	}).on('blur', ':input[id]', function(){
1112		var $label = $('label[for="' + $(this).attr('id') + '"]');
1113		$label.removeClass('focused');
1114	});
1115	
1116	/**
1117	 * add 'clear' button to text input field.
1118	 * need FontAwesome
1119	 * 
1120	 * usage:
1121	 * <input class="clearable">
1122	 */
1123	$(function(){
1124		var TEXT_INPUT_SELECTOR = 'input.clearable:not([type="checkbox"]):not([type="radio"]):not([type="number"])';
1125		
1126		$('<style id="style-input-clear"></style>').html([
1127			"#input-clear {",
1128			"	position:absolute;",
1129			"	z-index:99999;",
1130			"	background:transparent;",
1131			"	cursor:pointer;",
1132			"	border-style:solid;",
1133			"	border-color:transparent;",
1134			"	display:none;",
1135			"}",
1136			"#input-clear:before {",
1137			"	font-family:FontAwesome;",
1138			"	font-style:normal;",
1139			"	font-weight:normal;",
1140			"	text-decoration:normal;",
1141			"	opacity:.5;",
1142			// "	content:'\\f00d'; /* fa-times */",
1143			"	content:'\\f057'; /* fa-times-circle */",
1144			"}",
1145		].join('\n')).appendTo('head');
1146		
1147		$('#input-clear').remove();
1148		var $clear = $('<span id="input-clear"></span>').appendTo('body');
1149		
1150		function addClearToTextInput($input){
1151			if ($input.is(':not([data-style])')) {
1152				$input.attr('data-style', $input.attr('style')||'');
1153				var pr = parseInt($input.css('padding-right'), 0),
1154					pt = parseInt($input.css('padding-top'), 0),
1155					bt = parseInt($input.css('border-top-width'), 0),
1156					br = parseInt($input.css('border-right-width'), 0),
1157					fs = parseInt($input.css('font-size'), 0),
1158					lh = parseInt($input.css('line-height'), 0),
1159					cl = $input.css('color');
1160			}
1161			var box = $input[0].getBoundingClientRect();
1162			$clear
1163				.data('$input', $input)
1164				.css({
1165					'top':$(window).scrollTop() + box.top + 'px',
1166					'left':$(window).scrollLeft() + box.left + box.width + 'px',
1167					'font-size': fs + 'px',
1168					'line-height': lh + 'px',
1169					'padding-top': pt + 'px',
1170					'padding-right': pr + 'px',
1171					'padding-bottom': pt + 'px',
1172					'border-top-width': bt + 'px',
1173					'border-right-width': bt + 'px',
1174					'color':cl,
1175				})
1176				.toggle(true);
1177			var cw = $clear[0].getBoundingClientRect().width;
1178			$clear.css('margin-left', - cw + 'px');
1179			$input.css('padding-right', cw + 'px');
1180		}
1181		function removeClearToTextInput($input){
1182			$clear.toggle(false).data('$input', $());
1183			if ($input.is('[data-style]')) {
1184				$input.attr('style', $input.attr('data-style'));
1185			}
1186		}
1187		$(document)
1188			.on('focus', TEXT_INPUT_SELECTOR, function(){
1189				addClearToTextInput($(this));
1190			})
1191			.on('blur', TEXT_INPUT_SELECTOR, function(){
1192				// removeClearToTextInput($(this));
1193			})
1194			.on('focus click keydown', TEXT_INPUT_SELECTOR, function(){
1195				$clear
1196					.toggle($(this).val() != '');
1197			});
1198		$clear
1199			.data('$input', $())
1200			.on('click', function(){
1201				var $input = $(this).data('$input');
1202				$input.val('').trigger('focus');
1203			});
1204	});
1205
1206})(jQuery);

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.