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.