1/*! X-editable - v1.4.6 2* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery 3* http://github.com/vitalets/x-editable 4* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ 5 6/** 7Form with single input element, two buttons and two states: normal/loading. 8Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown. 9Editableform is linked with one of input types, e.g. 'text', 'select' etc. 10 11@class editableform 12@uses text 13@uses textarea 14**/ 15(function ($) { 16 "use strict"; 17 18 var EditableForm = function (div, options) { 19 this.options = $.extend({}, $.fn.editableform.defaults, options); 20 this.$div = $(div); //div, containing form. Not form tag. Not editable-element. 21 if(!this.options.scope) { 22 this.options.scope = this; 23 } 24 //nothing shown after init 25 }; 26 27 EditableForm.prototype = { 28 constructor: EditableForm, 29 initInput: function() { //called once 30 //take input from options (as it is created in editable-element) 31 this.input = this.options.input; 32 33 //set initial value 34 //todo: may be add check: typeof str === 'string' ? 35 this.value = this.input.str2value(this.options.value); 36 }, 37 initTemplate: function() { 38 this.$form = $($.fn.editableform.template); 39 }, 40 initButtons: function() { 41 var $btn = this.$form.find('.editable-buttons'); 42 $btn.append($.fn.editableform.buttons); 43 if(this.options.showbuttons === 'bottom') { 44 $btn.addClass('editable-buttons-bottom'); 45 } 46 }, 47 /** 48 Renders editableform 49 50 @method render 51 **/ 52 render: function() { 53 //init loader 54 this.$loading = $($.fn.editableform.loading); 55 this.$div.empty().append(this.$loading); 56 57 //init form template and buttons 58 this.initTemplate(); 59 if(this.options.showbuttons) { 60 this.initButtons(); 61 } else { 62 this.$form.find('.editable-buttons').remove(); 63 } 64 65 //show loading state 66 this.showLoading(); 67 68 //flag showing is form now saving value to server. 69 //It is needed to wait when closing form. 70 this.isSaving = false; 71 72 /** 73 Fired when rendering starts 74 @event rendering 75 @param {Object} event event object 76 **/ 77 this.$div.triggerHandler('rendering'); 78 79 //init input 80 this.initInput(); 81 82 //append input to form 83 this.input.prerender(); 84 this.$form.find('div.editable-input').append(this.input.$tpl); 85 86 //append form to container 87 this.$div.append(this.$form); 88 89 //render input 90 $.when(this.input.render()) 91 .then($.proxy(function () { 92 //setup input to submit automatically when no buttons shown 93 if(!this.options.showbuttons) { 94 this.input.autosubmit(); 95 } 96 97 //attach 'cancel' handler 98 this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); 99 100 if(this.input.error) { 101 this.error(this.input.error); 102 this.$form.find('.editable-submit').attr('disabled', true); 103 this.input.$input.attr('disabled', true); 104 //prevent form from submitting 105 this.$form.submit(function(e){ e.preventDefault(); }); 106 } else { 107 this.error(false); 108 this.input.$input.removeAttr('disabled'); 109 this.$form.find('.editable-submit').removeAttr('disabled'); 110 var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value; 111 this.input.value2input(value); 112 //attach submit handler 113 this.$form.submit($.proxy(this.submit, this)); 114 } 115 116 /** 117 Fired when form is rendered 118 @event rendered 119 @param {Object} event event object 120 **/ 121 this.$div.triggerHandler('rendered'); 122
123 this.showForm(); 124 125 //call postrender method to perform actions required visibility of form 126 if(this.input.postrender) { 127 this.input.postrender(); 128 } 129 }, this)); 130 }, 131 cancel: function() { 132 /** 133 Fired when form was cancelled by user 134 @event cancel 135 @param {Object} event event object 136 **/ 137 this.$div.triggerHandler('cancel'); 138 }, 139 showLoading: function() { 140 var w, h; 141 if(this.$form) { 142 //set loading size equal to form 143 w = this.$form.outerWidth(); 144 h = this.$form.outerHeight(); 145 if(w) { 146 this.$loading.width(w); 147 } 148 if(h) { 149 this.$loading.height(h); 150 } 151 this.$form.hide(); 152 } else { 153 //stretch loading to fill container width 154 w = this.$loading.parent().width(); 155 if(w) { 156 this.$loading.width(w); 157 } 158 } 159 this.$loading.show(); 160 }, 161 162 showForm: function(activate) { 163 this.$loading.hide(); 164 this.$form.show(); 165 if(activate !== false) { 166 this.input.activate(); 167 } 168 /** 169 Fired when form is shown 170 @event show 171 @param {Object} event event object 172 **/ 173 this.$div.triggerHandler('show'); 174 }, 175 176 error: function(msg) { 177 var $group = this.$form.find('.control-group'), 178 $block = this.$form.find('.editable-error-block'), 179 lines; 180 181 if(msg === false) { 182 $group.removeClass($.fn.editableform.errorGroupClass); 183 $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); 184 } else { 185 //convert newline to <br> for more pretty error display 186 if(msg) { 187 lines = msg.split("\n"); 188 for (var i = 0; i < lines.length; i++) { 189 lines[i] = $('<div>').text(lines[i]).html(); 190 } 191 msg = lines.join('<br>'); 192 } 193 $group.addClass($.fn.editableform.errorGroupClass); 194 $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); 195 } 196 }, 197 198 submit: function(e) { 199 e.stopPropagation(); 200 e.preventDefault(); 201 202 var error, 203 newValue = this.input.input2value(); //get new value from input 204 205 //validation 206 if (error = this.validate(newValue)) { 207 this.error(error); 208 this.showForm(); 209 return; 210 } 211 212 //if value not changed --> trigger 'nochange' event and return 213 /*jslint eqeq: true*/ 214 if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) { 215 /*jslint eqeq: false*/ 216 /** 217 Fired when value not changed but form is submitted. Requires savenochange = false. 218 @event nochange 219 @param {Object} event event object 220 **/ 221 this.$div.triggerHandler('nochange'); 222 return; 223 } 224 225 //convert value for submitting to server 226 var submitValue = this.input.value2submit(newValue); 227 228 this.isSaving = true; 229 230 //sending data to server 231 $.when(this.save(submitValue)) 232 .done($.proxy(function(response) { 233 this.isSaving = false; 234 235 //run success callback 236 var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; 237 238 //if success callback returns false --> keep form open and do not activate input 239 if(res === false) { 240 this.error(false);
241 this.showForm(false); 242 return; 243 } 244 245 //if success callback returns string --> keep form open, show error and activate input 246 if(typeof res === 'string') { 247 this.error(res); 248 this.showForm(); 249 return; 250 } 251 252 //if success callback returns object like {newValue: <something>} --> use that value instead of submitted 253 //it is usefull if you want to chnage value in url-function 254 if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { 255 newValue = res.newValue; 256 } 257 258 //clear error message 259 this.error(false); 260 this.value = newValue; 261 /** 262 Fired when form is submitted 263 @event save 264 @param {Object} event event object 265 @param {Object} params additional params 266 @param {mixed} params.newValue raw new value 267 @param {mixed} params.submitValue submitted value as string 268 @param {Object} params.response ajax response 269 270 @example 271 $('#form-div').on('save'), function(e, params){ 272 if(params.newValue === 'username') {...} 273 }); 274 **/ 275 this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); 276 }, this)) 277 .fail($.proxy(function(xhr) { 278 this.isSaving = false; 279 280 var msg; 281 if(typeof this.options.error === 'function') { 282 msg = this.options.error.call(this.options.scope, xhr, newValue); 283 } else { 284 msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; 285 } 286 287 this.error(msg); 288 this.showForm(); 289 }, this)); 290 }, 291 292 save: function(submitValue) { 293 //try parse composite pk defined as json string in data-pk 294 this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); 295 296 var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, 297 /* 298 send on server in following cases: 299 1. url is function 300 2. url is string AND (pk defined OR send option = always) 301 */ 302 send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), 303 params; 304 305 if (send) { //send to server 306 this.showLoading(); 307 308 //standard params 309 params = { 310 name: this.options.name || '', 311 value: submitValue, 312 pk: pk 313 }; 314 315 //additional params 316 if(typeof this.options.params === 'function') { 317 params = this.options.params.call(this.options.scope, params); 318 } else { 319 //try parse json in single quotes (from data-params attribute) 320 this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); 321 $.extend(params, this.options.params); 322 } 323 324 if(typeof this.options.url === 'function') { //user's function 325 return this.options.url.call(this.options.scope, params); 326 } else { 327 //send ajax to server and return deferred object 328 return $.ajax($.extend({ 329 url : this.options.url, 330 data : params, 331 type : 'POST' 332 }, this.options.ajaxOptions)); 333 } 334 } 335 }, 336 337 validate: function (value) { 338 if (value === undefined) { 339 value = this.value; 340 } 341 if (typeof this.options.validate === 'function') { 342 return this.options.validate.call(this.options.scope, value); 343 } 344 }, 345 346 option: function(key, value) { 347 if(key in this.options) { 348 this.options[key] = value; 349 } 350 351 if(key === 'value') { 352 this.setValue(value); 353 } 354 355 //do not pass option to input as it is passed in editable-element 356 }, 357 358 setValue: function(value, convertStr) { 359 if(convertStr) { 360 this.value = this.input.str2value(value); 361 } else { 362 this.value = value; 363 } 364 365 //if form is visible, update input 366 if(this.$form && this.$form.is(':visible')) { 367 this.input.value2input(this.value); 368 } 369 } 370 }; 371 372 /* 373 Initialize editableform. Applied to jQuery object. 374 375 @method $().editableform(options) 376 @params {Object} options 377 @example 378 var $form = $('<div>').editableform({ 379 type: 'text', 380 name: 'username', 381 url: '/post', 382 value: 'vitaliy' 383 }); 384 385 //to display form you should call 'render' method 386 $form.editableform('render'); 387 */ 388 $.fn.editableform = function (option) { 389 var args = arguments; 390 return this.each(function () { 391 var $this = $(this), 392 data = $this.data('editableform'), 393 options = typeof option === 'object' && option; 394 if (!data) { 395 $this.data('editableform', (data = new EditableForm(this, options))); 396 } 397 398 if (typeof option === 'string') { //call method 399 data[option].apply(data, Array.prototype.slice.call(args, 1)); 400 } 401 }); 402 }; 403 404 //keep link to constructor to allow inheritance 405 $.fn.editableform.Constructor = EditableForm; 406 407 //defaults 408 $.fn.editableform.defaults = { 409 /* see also defaults for input */ 410 411 /** 412 Type of input. Can be <code>
412text|textarea|select|date|checklist</code> 413 414 @property type 415 @type string 416 @default 'text' 417 **/ 418 type: 'text', 419 /** 420 Url for submit, e.g. <code>'/post'</code> 421 If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks. 422 423 @property url 424 @type string|function 425 @default null 426 @example 427 url: function(params) { 428 var d = new $.Deferred; 429 if(params.value === 'abc') { 430 return d.reject('error message'); //returning error via deferred object 431 } else { 432 //async saving data in js model 433 someModel.asyncSaveMethod({ 434 ..., 435 success: function(){ 436 d.resolve(); 437 } 438 }); 439 return d.promise(); 440 } 441 } 442 **/ 443 url:null, 444 /** 445 Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value). 446 If defined as <code>function</code> - returned object **overwrites** original ajax data. 447 @example 448 params: function(params) { 449 //originally params contain pk, name and value 450 params.a = 1; 451 return params; 452 } 453 454 @property params 455 @type object|function 456 @default null 457 **/ 458 params:null, 459 /** 460 Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute 461 462 @property name 463 @type string 464 @default null 465 **/ 466 name: null, 467 /** 468 Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>. 469 Can be calculated dynamically via function. 470 471 @property pk 472 @type string|object|function 473 @default null 474 **/ 475 pk: null, 476 /** 477 Initial value. If not defined - will be taken from element's content. 478 For __select__ type should be defined (as it is ID of shown text). 479 480 @property value 481 @type string|object 482 @default null 483 **/ 484 value: null, 485 /** 486 Value that will be displayed in input if original field value is empty (`null|undefined|''`). 487 488 @property defaultValue 489 @type string|object 490 @default null 491 @since 1.4.6 492 **/ 493 defaultValue: null, 494 /** 495 Strategy for sending data on server. Can be `auto|always|never`. 496 When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally. 497 498 @property send 499 @type string 500 @default 'auto' 501 **/ 502 send: 'auto', 503 /** 504 Function for client-side validation. If returns string - means validation not passed and string showed as error. 505 506 @property validate 507 @type function 508 @default null 509 @example 510 validate: function(value) { 511 if($.trim(value) == '') { 512 return 'This field is required'; 513 } 514 } 515 **/ 516 validate: null, 517 /** 518 Success callback. Called when value successfully sent on server and **response status = 200**. 519 Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code> 520 or <code>{success: false, msg: "server error"}</code> you can check it inside this callback. 521 If it returns **string** - means error occured and string is shown as error message. 522 If it returns **object like** <code>{newValue: <something>}</code> - it overwrites value, submitted by user. 523 Otherwise newValue simply rendered into element. 524 525 @property success 526 @type function 527 @default null 528 @example 529 success: function(response, newValue) { 530 if(!response.success) return response.msg; 531 } 532 **/ 533 success: null, 534 /** 535 Error callback. Called when request failed (response status != 200). 536 Usefull when you want to parse error response and display a custom message. 537 Must return **string** - the message to be displayed in the error block. 538 539 @property error 540 @type function 541 @default null 542 @since 1.4.4 543 @example 544 error: function(response, newValue) { 545 if(response.status === 500) { 546 return 'Service unavailable. Please try later.'; 547 } else { 548 return response.responseText; 549 } 550 } 551 **/ 552 error: null, 553 /** 554 Additional options for submit ajax request. 555 List of values: http://api.jquery.com/jQuery.ajax 556 557 @property ajaxOptions 558 @type object 559 @default null 560 @since 1.1.1 561 @example 562 ajaxOptions: { 563 type: 'put', 564 dataType: 'json' 565 } 566 **/ 567 ajaxOptions: null, 568 /** 569 Where to show buttons: left(true)|bottom|false 570 Form without buttons is auto-submitted. 571 572 @property showbuttons 573 @type boolean|string 574 @default true 575 @since 1.1.1 576 **/ 577 showbuttons: true, 578 /** 579 Scope for callback methods (success, validate). 580 If <code>null</code> means editableform instance itself.
581 582 @property scope 583 @type DOMElement|object 584 @default null 585 @since 1.2.0 586 @private 587 **/ 588 scope: null, 589 /** 590 Whether to save or cancel value when it was not changed but form was submitted 591 592 @property savenochange 593 @type boolean 594 @default false 595 @since 1.2.0 596 **/ 597 savenochange: false 598 }; 599 600 /* 601 Note: following params could redefined in engine: bootstrap or jqueryui: 602 Classes 'control-group' and 'editable-error-block' must always present! 603 */ 604 $.fn.editableform.template = '<form class="form-inline editableform">'+ 605 '<div class="control-group">' + 606 '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+ 607 '<div class="editable-error-block"></div>' + 608 '</div>' + 609 '</form>'; 610 611 //loading div 612 $.fn.editableform.loading = '<div class="editableform-loading"></div>'; 613 614 //buttons 615 $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+ 616 '<button type="button" class="editable-cancel">cancel</button>'; 617 618 //error class attached to control-group 619 $.fn.editableform.errorGroupClass = null; 620 621 //error class attached to editable-error-block 622 $.fn.editableform.errorBlockClass = 'editable-error'; 623}(window.jQuery)); 624 625/** 626* EditableForm utilites 627*/ 628(function ($) { 629 "use strict"; 630 631 //utils 632 $.fn.editableutils = { 633 /** 634 * classic JS inheritance function 635 */ 636 inherit: function (Child, Parent) { 637 var F = function() { }; 638 F.prototype = Parent.prototype; 639 Child.prototype = new F(); 640 Child.prototype.constructor = Child; 641 Child.superclass = Parent.prototype; 642 }, 643 644 /** 645 * set caret position in input 646 * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area 647 */ 648 setCursorPosition: function(elem, pos) { 649 if (elem.setSelectionRange) { 650 elem.setSelectionRange(pos, pos); 651 } else if (elem.createTextRange) { 652 var range = elem.createTextRange(); 653 range.collapse(true); 654 range.moveEnd('character', pos); 655 range.moveStart('character', pos); 656 range.select(); 657 } 658 }, 659 660 /** 661 * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes) 662 * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}"> 663 * safe = true --> means no exception will be thrown 664 * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery 665 */ 666 tryParseJson: function(s, safe) { 667 if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) { 668 if (safe) { 669 try { 670 /*jslint evil: true*/ 671 s = (new Function('return ' + s))(); 672 /*jslint evil: false*/ 673 } catch (e) {} finally { 674 return s; 675 } 676 } else { 677 /*jslint evil: true*/ 678 s = (new Function('return ' + s))(); 679 /*jslint evil: false*/ 680 } 681 } 682 return s; 683 }, 684 685 /** 686 * slice object by specified keys 687 */ 688 sliceObj: function(obj, keys, caseSensitive /* default: false */) { 689 var key, keyLower, newObj = {}; 690 691 if (!$.isArray(keys) || !keys.length) { 692 return newObj; 693 } 694 695 for (var i = 0; i < keys.length; i++) { 696 key = keys[i]; 697 if (obj.hasOwnProperty(key)) { 698 newObj[key] = obj[key]; 699 } 700 701 if(caseSensitive === true) { 702 continue; 703 } 704 705 //when getting data-* attributes via $.data() it's converted to lowercase. 706 //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery 707 //workaround is code below. 708 keyLower = key.toLowerCase(); 709 if (obj.hasOwnProperty(keyLower)) { 710 newObj[key] = obj[keyLower]; 711 } 712 } 713 714 return newObj; 715 }, 716 717 /* 718 exclude complex objects from $.data() before pass to config 719 */ 720 getConfigData: function($element) { 721 var data = {}; 722 $.each($element.data(), function(k, v) { 723 if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) { 724 data[k] = v; 725 } 726 }); 727 return data; 728 }, 729 730 /* 731 returns keys of object 732 */ 733 objectKeys: function(o) { 734 if (Object.keys) { 735 return Object.keys(o); 736 } else { 737 if (o !== Object(o)) { 738 throw new TypeError('Object.keys called on a non-object'); 739 } 740 var k=[], p; 741 for (p in o) { 742 if (Object.prototype.hasOwnProperty.call(o,p)) { 743 k.push(p); 744 } 745 } 746 return k; 747 } 748 749 }, 750 751 /** 752 method to escape html. 753 **/ 754 escape: function(str) { 755 return $('<div>').text(str).html(); 756 }, 757 758 /* 759 returns array items from sourceData having value property equal or inArray of 'value' 760 */ 761 itemsByValue: function(value, sourceData, valueProp) { 762 if(!sourceData || value === null) { 763 return []; 764 } 765 766 if (typeof(valueProp) !== "function") { 767 var idKey = valueProp || 'value'; 768 valueProp = function (e) { return e[idKey]; }; 769 } 770 771 var isValArray = $.isArray(value),
772 result = [], 773 that = this; 774 775 $.each(sourceData, function(i, o) { 776 if(o.children) { 777 result = result.concat(that.itemsByValue(value, o.children, valueProp)); 778 } else { 779 /*jslint eqeq: true*/ 780 if(isValArray) { 781 if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) { 782 result.push(o); 783 } 784 } else { 785 var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o; 786 if(value == itemValue) { 787 result.push(o); 788 } 789 } 790 /*jslint eqeq: false*/ 791 } 792 }); 793 794 return result; 795 }, 796 797 /* 798 Returns input by options: type, mode. 799 */ 800 createInput: function(options) { 801 var TypeConstructor, typeOptions, input, 802 type = options.type; 803 804 //`date` is some kind of virtual type that is transformed to one of exact types 805 //depending on mode and core lib 806 if(type === 'date') { 807 //inline 808 if(options.mode === 'inline') { 809 if($.fn.editabletypes.datefield) { 810 type = 'datefield'; 811 } else if($.fn.editabletypes.dateuifield) { 812 type = 'dateuifield'; 813 } 814 //popup 815 } else { 816 if($.fn.editabletypes.date) { 817 type = 'date'; 818 } else if($.fn.editabletypes.dateui) { 819 type = 'dateui'; 820 } 821 } 822 823 //if type still `date` and not exist in types, replace with `combodate` that is base input 824 if(type === 'date' && !$.fn.editabletypes.date) { 825 type = 'combodate'; 826 } 827 } 828 829 //`datetime` should be datetimefield in 'inline' mode 830 if(type === 'datetime' && options.mode === 'inline') { 831 type = 'datetimefield'; 832 } 833 834 //change wysihtml5 to textarea for jquery UI and plain versions 835 if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { 836 type = 'textarea'; 837 } 838 839 //create input of specified type. Input will be used for converting value, not in form 840 if(typeof $.fn.editabletypes[type] === 'function') { 841 TypeConstructor = $.fn.editabletypes[type]; 842 typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults)); 843 input = new TypeConstructor(typeOptions); 844 return input; 845 } else { 846 $.error('Unknown type: '+ type); 847 return false; 848 } 849 }, 850 851 //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr 852 supportsTransitions: function () { 853 var b = document.body || document.documentElement, 854 s = b.style, 855 p = 'transition', 856 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; 857 858 if(typeof s[p] === 'string') { 859 return true; 860 } 861 862 // Tests for vendor specific prop 863 p = p.charAt(0).toUpperCase() + p.substr(1); 864 for(var i=0; i<v.length; i++) { 865 if(typeof s[v[i] + p] === 'string') { 866 return true; 867 } 868 } 869 return false; 870 } 871 872 }; 873}(window.jQuery)); 874 875/** 876Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br> 877This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
878Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br> 879Applied as jQuery method. 880 881@class editableContainer 882@uses editableform 883**/ 884(function ($) { 885 "use strict"; 886 887 var Popup = function (element, options) { 888 this.init(element, options); 889 }; 890 891 var Inline = function (element, options) { 892 this.init(element, options); 893 }; 894 895 //methods 896 Popup.prototype = { 897 containerName: null, //method to call container on element 898 containerDataName: null, //object name in element's .data() 899 innerCss: null, //tbd in child class 900 containerClass: 'editable-container editable-popup', //css class applied to container element 901 init: function(element, options) { 902 this.$element = $(element); 903 //since 1.4.1 container do not use data-* directly as they already merged into options. 904 this.options = $.extend({}, $.fn.editableContainer.defaults, options); 905 this.splitOptions(); 906 907 //set scope of form callbacks to element 908 this.formOptions.scope = this.$element[0]; 909 910 this.initContainer(); 911 912 //flag to hide container, when saving value will finish 913 this.delayedHide = false;
914 915 //bind 'destroyed' listener to destroy container when element is removed from dom 916 this.$element.on('destroyed', $.proxy(function(){ 917 this.destroy(); 918 }, this)); 919 920 //attach document handler to close containers on click / escape 921 if(!$(document).data('editable-handlers-attached')) { 922 //close all on escape 923 $(document).on('keyup.editable', function (e) { 924 if (e.which === 27) { 925 $('.editable-open').editableContainer('hide'); 926 //todo: return focus on element 927 } 928 }); 929 930 //close containers when click outside 931 //(mousedown could be better than click, it closes everything also on drag drop) 932 $(document).on('click.editable', function(e) { 933 var $target = $(e.target), i, 934 exclude_classes = ['.editable-container', 935 '.ui-datepicker-header', 936 '.datepicker', //in inline mode datepicker is rendered into body 937 '.modal-backdrop', 938 '.bootstrap-wysihtml5-insert-image-modal', 939 '.bootstrap-wysihtml5-insert-link-modal' 940 ]; 941 942 //check if element is detached. It occurs when clicking in bootstrap datepicker 943 if (!$.contains(document.documentElement, e.target)) { 944 return; 945 } 946 947 //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document 948 //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 949 //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec 950 if($target.is(document)) { 951 return; 952 } 953 954 //if click inside one of exclude classes --> no nothing 955 for(i=0; i<exclude_classes.length; i++) { 956 if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) { 957 return; 958 } 959 } 960 961 //close all open containers (except one - target) 962 Popup.prototype.closeOthers(e.target); 963 }); 964 965 $(document).data('editable-handlers-attached', true); 966 } 967 }, 968 969 //split options on containerOptions and formOptions 970 splitOptions: function() { 971 this.containerOptions = {}; 972 this.formOptions = {}; 973 974 if(!$.fn[this.containerName]) { 975 throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); 976 } 977 978 var cDef = $.fn[this.containerName].defaults; 979 //keys defined in container defaults go to container, others go to form 980 for(var k in this.options) { 981 if(k in cDef) { 982 this.containerOptions[k] = this.options[k]; 983 } else { 984 this.formOptions[k] = this.options[k]; 985 } 986 } 987 }, 988 989 /* 990 Returns jquery object of container 991 @method tip() 992 */ 993 tip: function() { 994 return this.container() ? this.container().$tip : null; 995 }, 996 997 /* returns container object */ 998 container: function() { 999 var container; 1000 //first, try get it by `containerDataName` 1001 if(this.containerDataName) { 1002 if(container = this.$element.data(this.containerDataName)) { 1003 return container; 1004 } 1005 }
1006 //second, try `containerName` 1007 container = this.$element.data(this.containerName); 1008 return container; 1009 }, 1010 1011 /* call native method of underlying container, e.g. this.$element.popover('method') */ 1012 call: function() { 1013 this.$element[this.containerName].apply(this.$element, arguments); 1014 }, 1015 1016 initContainer: function(){ 1017 this.call(this.containerOptions); 1018 }, 1019 1020 renderForm: function() { 1021 this.$form 1022 .editableform(this.formOptions) 1023 .on({ 1024 save: $.proxy(this.save, this), //click on submit button (value changed) 1025 nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed) 1026 cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button 1027 show: $.proxy(function() { 1028 if(this.delayedHide) { 1029 this.hide(this.delayedHide.reason); 1030 this.delayedHide = false;
1031 } else { 1032 this.setPosition(); 1033 } 1034 }, this), //re-position container every time form is shown (occurs each time after loading state) 1035 rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown 1036 resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed 1037 rendered: $.proxy(function(){ 1038 /** 1039 Fired when container is shown and form is rendered (for select will wait for loading dropdown options). 1040 **Note:** Bootstrap popover has own `shown` event that now cannot be separated from x-editable's one. 1041 The workaround is to check `arguments.length` that is always `2` for x-editable. 1042 1043 @event shown 1044 @param {Object} event event object 1045 @example 1046 $('#username').on('shown', function(e, editable) { 1047 editable.input.$input.val('overwriting value of input..'); 1048 }); 1049 **/ 1050 /* 1051 TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events. 1052 */ 1053 this.$element.triggerHandler('shown', $(this.options.scope).data('editable')); 1054 }, this) 1055 }) 1056 .editableform('render'); 1057 }, 1058 1059 /** 1060 Shows container with form 1061 @method show() 1062 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1063 **/ 1064 /* Note: poshytip owerwrites this method totally! */ 1065 show: function (closeAll) { 1066 this.$element.addClass('editable-open'); 1067 if(closeAll !== false) { 1068 //close all open containers (except this) 1069 this.closeOthers(this.$element[0]); 1070 } 1071 1072 //show container itself 1073 this.innerShow(); 1074 this.tip().addClass(this.containerClass); 1075 1076 /* 1077 Currently, form is re-rendered on every show. 1078 The main reason is that we dont know, what will container do with content when closed: 1079 remove(), detach() or just hide() - it depends on container. 1080 1081 Detaching form itself before hide and re-insert before show is good solution, 1082 but visually it looks ugly --> container changes size before hide. 1083 */ 1084 1085 //if form already exist - delete previous data 1086 if(this.$form) { 1087 //todo: destroy prev data! 1088 //this.$form.destroy(); 1089 } 1090 1091 this.$form = $('<div>'); 1092 1093 //insert form into container body 1094 if(this.tip().is(this.innerCss)) { 1095 //for inline container 1096 this.tip().append(this.$form); 1097 } else { 1098 this.tip().find(this.innerCss).append(this.$form); 1099 } 1100 1101 //render form 1102 this.renderForm(); 1103 }, 1104 1105 /** 1106 Hides container with form 1107 @method hide() 1108 @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code> 1109 **/ 1110 hide: function(reason) { 1111 if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { 1112 return; 1113 } 1114 1115 //if form is saving value, schedule hide 1116 if(this.$form.data('editableform').isSaving) { 1117 this.delayedHide = {reason: reason}; 1118 return; 1119 } else { 1120 this.delayedHide = false;
1121 } 1122 1123 this.$element.removeClass('editable-open'); 1124 this.innerHide(); 1125 1126 /** 1127 Fired when container was hidden. It occurs on both save or cancel. 1128 **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. 1129 The workaround is to check `arguments.length` that is always `2` for x-editable. 1130 1131 @event hidden 1132 @param {object} event event object 1133 @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|manual</code> 1134 @example 1135 $('#username').on('hidden', function(e, reason) { 1136 if(reason === 'save' || reason === 'cancel') { 1137 //auto-open next editable 1138 $(this).closest('tr').next().find('.editable').editable('show'); 1139 } 1140 }); 1141 **/ 1142 this.$element.triggerHandler('hidden', reason || 'manual'); 1143 }, 1144 1145 /* internal show method. To be overwritten in child classes */ 1146 innerShow: function () { 1147 1148 }, 1149 1150 /* internal hide method. To be overwritten in child classes */ 1151 innerHide: function () { 1152 1153 }, 1154 1155 /** 1156 Toggles container visibility (show / hide) 1157 @method toggle() 1158 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1159 **/ 1160 toggle: function(closeAll) { 1161 if(this.container() && this.tip() && this.tip().is(':visible')) { 1162 this.hide(); 1163 } else { 1164 this.show(closeAll); 1165 } 1166 }, 1167 1168 /* 1169 Updates the position of container when content changed. 1170 @method setPosition() 1171 */ 1172 setPosition: function() { 1173 //tbd in child class 1174 }, 1175 1176 save: function(e, params) { 1177 /** 1178 Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance 1179 1180 @event save 1181 @param {Object} event event object 1182 @param {Object} params additional params 1183 @param {mixed} params.newValue submitted value 1184 @param {Object} params.response ajax response 1185 @example 1186 $('#username').on('save', function(e, params) { 1187 //assuming server response: '{success: true}' 1188 var pk = $(this).data('editableContainer').options.pk; 1189 if(params.response && params.response.success) { 1190 alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); 1191 } else { 1192 alert('error!'); 1193 } 1194 }); 1195 **/ 1196 this.$element.triggerHandler('save', params); 1197 1198 //hide must be after trigger, as saving value may require methods of plugin, applied to input 1199 this.hide('save'); 1200 }, 1201 1202 /** 1203 Sets new option 1204 1205 @method option(key, value) 1206 @param {string} key 1207 @param {mixed} value 1208 **/ 1209 option: function(key, value) { 1210 this.options[key] = value; 1211 if(key in this.containerOptions) { 1212 this.containerOptions[key] = value; 1213 this.setContainerOption(key, value); 1214 } else { 1215 this.formOptions[key] = value; 1216 if(this.$form) { 1217 this.$form.editableform('option', key, value); 1218 } 1219 } 1220 }, 1221 1222 setContainerOption: function(key, value) { 1223 this.call('option', key, value); 1224 }, 1225 1226 /** 1227 Destroys the container instance 1228 @method destroy() 1229 **/ 1230 destroy: function() { 1231 this.hide(); 1232 this.innerDestroy(); 1233 this.$element.off('destroyed'); 1234 this.$element.removeData('editableContainer'); 1235 }, 1236
1237 /* to be overwritten in child classes */ 1238 innerDestroy: function() { 1239 1240 }, 1241 1242 /* 1243 Closes other containers except one related to passed element. 1244 Other containers can be cancelled or submitted (depends on onblur option) 1245 */ 1246 closeOthers: function(element) { 1247 $('.editable-open').each(function(i, el){ 1248 //do nothing with passed element and it's children 1249 if(el === element || $(el).find(element).length) { 1250 return; 1251 } 1252 1253 //otherwise cancel or submit all open containers 1254 var $el = $(el), 1255 ec = $el.data('editableContainer'); 1256 1257 if(!ec) { 1258 return; 1259 } 1260 1261 if(ec.options.onblur === 'cancel') { 1262 $el.data('editableContainer').hide('onblur'); 1263 } else if(ec.options.onblur === 'submit') { 1264 $el.data('editableContainer').tip().find('form').submit(); 1265 } 1266 }); 1267 1268 }, 1269 1270 /** 1271 Activates input of visible container (e.g. set focus) 1272 @method activate() 1273 **/ 1274 activate: function() { 1275 if(this.tip && this.tip().is(':visible') && this.$form) { 1276 this.$form.data('editableform').input.activate(); 1277 } 1278 } 1279 1280 }; 1281 1282 /** 1283 jQuery method to initialize editableContainer. 1284 1285 @method $().editableContainer(options) 1286 @params {Object} options 1287 @example 1288 $('#edit').editableContainer({ 1289 type: 'text', 1290 url: '/post', 1291 pk: 1, 1292 value: 'hello' 1293 }); 1294 **/ 1295 $.fn.editableContainer = function (option) { 1296 var args = arguments; 1297 return this.each(function () { 1298 var $this = $(this), 1299 dataKey = 'editableContainer', 1300 data = $this.data(dataKey), 1301 options = typeof option === 'object' && option, 1302 Constructor = (options.mode === 'inline') ? Inline : Popup; 1303 1304 if (!data) { 1305 $this.data(dataKey, (data = new Constructor(this, options))); 1306 } 1307 1308 if (typeof option === 'string') { //call method 1309 data[option].apply(data, Array.prototype.slice.call(args, 1)); 1310 } 1311 }); 1312 }; 1313 1314 //store constructors 1315 $.fn.editableContainer.Popup = Popup; 1316 $.fn.editableContainer.Inline = Inline; 1317 1318 //defaults 1319 $.fn.editableContainer.defaults = { 1320 /** 1321 Initial value of form input 1322 1323 @property value 1324 @type mixed 1325 @default null 1326 @private 1327 **/ 1328 value: null, 1329 /** 1330 Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container. 1331 1332 @property placement 1333 @type string 1334 @default 'top' 1335 **/ 1336 placement: 'top', 1337 /** 1338 Whether to hide container on save/cancel. 1339 1340 @property autohide 1341 @type boolean 1342 @default true 1343 @private 1344 **/ 1345 autohide: true, 1346 /** 1347 Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>. 1348 Setting <code>ignore</code> allows to have several containers open. 1349 1350 @property onblur 1351 @type string 1352 @default 'cancel' 1353 @since 1.1.1 1354 **/ 1355 onblur: 'cancel', 1356 1357 /** 1358 Animation speed (inline mode only) 1359 @property anim 1360 @type string 1361 @default false 1362 **/ 1363 anim: false, 1364 1365 /** 1366 Mode of editable, can be `popup` or `inline` 1367 1368 @property mode 1369 @type string 1370 @default 'popup' 1371 @since 1.4.0 1372 **/ 1373 mode: 'popup' 1374 }; 1375 1376 /* 1377 * workaround to have 'destroyed' event to destroy popover when element is destroyed 1378 * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom 1379 */ 1380 jQuery.event.special.destroyed = { 1381 remove: function(o) { 1382 if (o.handler) { 1383 o.handler(); 1384 } 1385 } 1386 }; 1387 1388}(window.jQuery)); 1389 1390/** 1391* Editable Inline 1392* --------------------- 1393*/ 1394(function ($) { 1395 "use strict"; 1396 1397 //copy prototype from EditableContainer 1398 //extend methods 1399 $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { 1400 containerName: 'editableform', 1401 innerCss: '.editable-inline', 1402 containerClass: 'editable-container editable-inline', //css class applied to container element 1403 1404 initContainer: function(){ 1405 //container is <span> element 1406 this.$tip = $('<span></span>'); 1407 1408 //convert anim to miliseconds (int) 1409 if(!this.options.anim) { 1410 this.options.anim = 0; 1411 } 1412 }, 1413 1414 splitOptions: function() { 1415 //all options are passed to form 1416 this.containerOptions = {}; 1417 this.formOptions = this.options; 1418 }, 1419 1420 tip: function() { 1421 return this.$tip; 1422 }, 1423 1424 innerShow: function () { 1425 this.$element.hide(); 1426 this.tip().insertAfter(this.$element).show(); 1427 }, 1428 1429 innerHide: function () { 1430 this.$tip.hide(this.options.anim, $.proxy(function() { 1431 this.$element.show(); 1432 this.innerDestroy(); 1433 }, this)); 1434 }, 1435 1436 innerDestroy: function() { 1437 if(this.tip()) { 1438 this.tip().empty().remove(); 1439 } 1440 } 1441 }); 1442 1443}(window.jQuery)); 1444/**
1445Makes editable any HTML element on the page. Applied as jQuery method. 1446 1447@class editable 1448@uses editableContainer 1449**/ 1450(function ($) { 1451 "use strict"; 1452 1453 var Editable = function (element, options) { 1454 this.$element = $(element); 1455 //data-* has more priority over js options: because dynamically created elements may change data-* 1456 this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); 1457 if(this.options.selector) { 1458 this.initLive(); 1459 } else { 1460 this.init(); 1461 } 1462 1463 //check for transition support 1464 if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { 1465 this.options.highlight = false; 1466 } 1467 }; 1468 1469 Editable.prototype = { 1470 constructor: Editable, 1471 init: function () { 1472 var isValueByText = false, 1473 doAutotext, finalize; 1474 1475 //name 1476 this.options.name = this.options.name || this.$element.attr('id'); 1477 1478 //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) 1479 //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) 1480 this.options.scope = this.$element[0]; 1481 this.input = $.fn.editableutils.createInput(this.options); 1482 if(!this.input) { 1483 return; 1484 } 1485 1486 //set value from settings or by element's text 1487 if (this.options.value === undefined || this.options.value === null) { 1488 this.value = this.input.html2value($.trim(this.$element.html())); 1489 isValueByText = true; 1490 } else { 1491 /* 1492 value can be string when received from 'data-value' attribute 1493 for complext objects value can be set as json string in data-value attribute, 1494 e.g. data-value="{city: 'Moscow', street: 'Lenina'}" 1495 */ 1496 this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); 1497 if(typeof this.options.value === 'string') { 1498 this.value = this.input.str2value(this.options.value); 1499 } else { 1500 this.value = this.options.value; 1501 } 1502 } 1503 1504 //add 'editable' class to every editable element 1505 this.$element.addClass('editable'); 1506 1507 //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks 1508 if(this.input.type === 'textarea') { 1509 this.$element.addClass('editable-pre-wrapped'); 1510 } 1511 1512 //attach handler activating editable. In disabled mode it just prevent default action (useful for links) 1513 if(this.options.toggle !== 'manual') { 1514 this.$element.addClass('editable-click'); 1515 this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ 1516 //prevent following link if editable enabled 1517 if(!this.options.disabled) { 1518 e.preventDefault(); 1519 } 1520 1521 //stop propagation not required because in document click handler it checks event target 1522 //e.stopPropagation(); 1523 1524 if(this.options.toggle === 'mouseenter') { 1525 //for hover only show container 1526 this.show(); 1527 } else { 1528 //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener 1529 var closeAll = (this.options.toggle !== 'click'); 1530 this.toggle(closeAll); 1531 } 1532 }, this)); 1533 } else { 1534 this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually 1535 } 1536 1537 //if display is function it's far more convinient to have autotext = always to render correctly on init 1538 //see https://github.com/vitalets/x-editable-yii/issues/34 1539 if(typeof this.options.display === 'function') { 1540 this.options.autotext = 'always'; 1541 } 1542 1543 //check conditions for autotext: 1544 switch(this.options.autotext) { 1545 case 'always': 1546 doAutotext = true; 1547 break; 1548 case 'auto': 1549 //if element text is empty and value is defined and value not generated by text --> run autotext 1550 doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; 1551 break; 1552 default: 1553 doAutotext = false;
1554 } 1555 1556 //depending on autotext run render() or just finilize init 1557 $.when(doAutotext ? this.render() : true).then($.proxy(function() { 1558 if(this.options.disabled) { 1559 this.disable(); 1560 } else { 1561 this.enable(); 1562 } 1563 /** 1564 Fired when element was initialized by `$().editable()` method. 1565 Please note that you should setup `init` handler **before** applying `editable`. 1566 1567 @event init 1568 @param {Object} event event object 1569 @param {Object} editable editable instance (as here it cannot accessed via data('editable')) 1570 @since 1.2.0 1571 @example 1572 $('#username').on('init', function(e, editable) { 1573 alert('initialized ' + editable.options.name); 1574 }); 1575 $('#username').editable(); 1576 **/ 1577 this.$element.triggerHandler('init', this); 1578 }, this)); 1579 }, 1580 1581 /* 1582 Initializes parent element for live editables 1583 */ 1584 initLive: function() { 1585 //store selector 1586 var selector = this.options.selector; 1587 //modify options for child elements 1588 this.options.selector = false; 1589 this.options.autotext = 'never'; 1590 //listen toggle events 1591 this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ 1592 var $target = $(e.target); 1593 if(!$target.data('editable')) { 1594 //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) 1595 //see https://github.com/vitalets/x-editable/issues/137 1596 if($target.hasClass(this.options.emptyclass)) { 1597 $target.empty(); 1598 } 1599 $target.editable(this.options).trigger(e); 1600 } 1601 }, this)); 1602 }, 1603 1604 /* 1605 Renders value into element's text. 1606 Can call custom display method from options. 1607 Can return deferred object. 1608 @method render() 1609 @param {mixed} response server response (if exist) to pass into display function 1610 */ 1611 render: function(response) { 1612 //do not display anything 1613 if(this.options.display === false) { 1614 return; 1615 } 1616 1617 //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded 1618 if(this.input.value2htmlFinal) { 1619 return this.input.value2html(this.value, this.$element[0], this.options.display, response); 1620 //if display method defined --> use it 1621 } else if(typeof this.options.display === 'function') { 1622 return this.options.display.call(this.$element[0], this.value, response); 1623 //else use input's original value2html() method 1624 } else { 1625 return this.input.value2html(this.value, this.$element[0]); 1626 } 1627 }, 1628 1629 /** 1630 Enables editable 1631 @method enable() 1632 **/ 1633 enable: function() { 1634 this.options.disabled = false; 1635 this.$element.removeClass('editable-disabled'); 1636 this.handleEmpty(this.isEmpty); 1637 if(this.options.toggle !== 'manual') { 1638 if(this.$element.attr('tabindex') === '-1') { 1639 this.$element.removeAttr('tabindex'); 1640 } 1641 } 1642 }, 1643 1644 /** 1645 Disables editable 1646 @method disable() 1647 **/ 1648 disable: function() { 1649 this.options.disabled = true; 1650 this.hide(); 1651 this.$element.addClass('editable-disabled'); 1652 this.handleEmpty(this.isEmpty); 1653 //do not stop focus on this element 1654 this.$element.attr('tabindex', -1); 1655 }, 1656 1657 /** 1658 Toggles enabled / disabled state of editable element 1659 @method toggleDisabled() 1660 **/ 1661 toggleDisabled: function() { 1662 if(this.options.disabled) { 1663 this.enable(); 1664 } else { 1665 this.disable(); 1666 } 1667 }, 1668 1669 /** 1670 Sets new option 1671 1672 @method option(key, value) 1673 @param {string|object} key option name or object with several options 1674 @param {mixed} value option new value 1675 @example 1676 $('.editable').editable('option', 'pk', 2); 1677 **/ 1678 option: function(key, value) { 1679 //set option(s) by object 1680 if(key && typeof key === 'object') { 1681 $.each(key, $.proxy(function(k, v){ 1682 this.option($.trim(k), v); 1683 }, this)); 1684 return; 1685 } 1686 1687 //set option by string 1688 this.options[key] = value; 1689 1690 //disabled 1691 if(key === 'disabled') { 1692 return value ? this.disable() : this.enable(); 1693 } 1694 1695 //value 1696 if(key === 'value') { 1697 this.setValue(value); 1698 } 1699 1700 //transfer new option to container! 1701 if(this.container) { 1702 this.container.option(key, value); 1703 } 1704 1705 //pass option to input directly (as it points to the same in form) 1706 if(this.input.option) { 1707 this.input.option(key, value); 1708 } 1709 1710 }, 1711 1712 /* 1713 * set emptytext if element is empty 1714 */ 1715 handleEmpty: function (isEmpty) { 1716 //do not handle empty if we do not display anything 1717 if(this.options.display === false) { 1718 return; 1719 } 1720 1721 /* 1722 isEmpty may be set directly as param of method. 1723 It is required when we enable/disable field and can't rely on content
1724 as node content is text: "Empty" that is not empty %) 1725 */ 1726 if(isEmpty !== undefined) { 1727 this.isEmpty = isEmpty; 1728 } else { 1729 //detect empty 1730 if($.trim(this.$element.html()) === '') { 1731 this.isEmpty = true; 1732 } else if($.trim(this.$element.text()) !== '') { 1733 this.isEmpty = false; 1734 } else { 1735 //e.g. '<img>' 1736 this.isEmpty = !this.$element.height() || !this.$element.width(); 1737 } 1738 } 1739 1740 //emptytext shown only for enabled 1741 if(!this.options.disabled) { 1742 if (this.isEmpty) { 1743 this.$element.html(this.options.emptytext); 1744 if(this.options.emptyclass) { 1745 this.$element.addClass(this.options.emptyclass); 1746 } 1747 } else if(this.options.emptyclass) { 1748 this.$element.removeClass(this.options.emptyclass); 1749 } 1750 } else { 1751 //below required if element disable property was changed 1752 if(this.isEmpty) { 1753 this.$element.empty(); 1754 if(this.options.emptyclass) { 1755 this.$element.removeClass(this.options.emptyclass); 1756 } 1757 } 1758 } 1759 }, 1760 1761 /** 1762 Shows container with form 1763 @method show() 1764 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1765 **/ 1766 show: function (closeAll) { 1767 if(this.options.disabled) { 1768 return; 1769 } 1770 1771 //init editableContainer: popover, tooltip, inline, etc.. 1772 if(!this.container) { 1773 var containerOptions = $.extend({}, this.options, { 1774 value: this.value, 1775 input: this.input //pass input to form (as it is already created) 1776 }); 1777 this.$element.editableContainer(containerOptions); 1778 //listen `save` event 1779 this.$element.on("save.internal", $.proxy(this.save, this)); 1780 this.container = this.$element.data('editableContainer'); 1781 } else if(this.container.tip().is(':visible')) { 1782 return; 1783 } 1784 1785 //show container 1786 this.container.show(closeAll); 1787 }, 1788 1789 /** 1790 Hides container with form 1791 @method hide() 1792 **/ 1793 hide: function () { 1794 if(this.container) { 1795 this.container.hide(); 1796 } 1797 }, 1798 1799 /** 1800 Toggles container visibility (show / hide) 1801 @method toggle() 1802 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1803 **/ 1804 toggle: function(closeAll) { 1805 if(this.container && this.container.tip().is(':visible')) { 1806 this.hide(); 1807 } else { 1808 this.show(closeAll); 1809 } 1810 }, 1811 1812 /* 1813 * called when form was submitted 1814 */ 1815 save: function(e, params) { 1816 //mark element with unsaved class if needed 1817 if(this.options.unsavedclass) { 1818 /* 1819 Add unsaved css to element if: 1820 - url is not user's function 1821 - value was not sent to server 1822 - params.response === undefined, that means data was not sent 1823 - value changed 1824 */ 1825 var sent = false; 1826 sent = sent || typeof this.options.url === 'function'; 1827 sent = sent || this.options.display === false; 1828 sent = sent || params.response !== undefined; 1829 sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); 1830 1831 if(sent) { 1832 this.$element.removeClass(this.options.unsavedclass); 1833 } else { 1834 this.$element.addClass(this.options.unsavedclass); 1835 } 1836 } 1837
1838 //highlight when saving 1839 if(this.options.highlight) { 1840 var $e = this.$element, 1841 bgColor = $e.css('background-color'); 1842 1843 $e.css('background-color', this.options.highlight); 1844 setTimeout(function(){ 1845 if(bgColor === 'transparent') { 1846 bgColor = ''; 1847 } 1848 $e.css('background-color', bgColor); 1849 $e.addClass('editable-bg-transition'); 1850 setTimeout(function(){ 1851 $e.removeClass('editable-bg-transition'); 1852 }, 1700); 1853 }, 10); 1854 } 1855 1856 //set new value 1857 this.setValue(params.newValue, false, params.response); 1858 1859 /** 1860 Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance 1861 1862 @event save 1863 @param {Object} event event object 1864 @param {Object} params additional params 1865 @param {mixed} params.newValue submitted value 1866 @param {Object} params.response ajax response 1867 @example 1868 $('#username').on('save', function(e, params) { 1869 alert('Saved value: ' + params.newValue); 1870 }); 1871 **/ 1872 //event itself is triggered by editableContainer. Description here is only for documentation 1873 }, 1874 1875 validate: function () { 1876 if (typeof this.options.validate === 'function') { 1877 return this.options.validate.call(this, this.value); 1878 } 1879 }, 1880 1881 /** 1882 Sets new value of editable 1883 @method setValue(value, convertStr) 1884 @param {mixed} value new value 1885 @param {boolean} convertStr whether to convert value from string to internal format 1886 **/ 1887 setValue: function(value, convertStr, response) { 1888 if(convertStr) { 1889 this.value = this.input.str2value(value); 1890 } else { 1891 this.value = value; 1892 } 1893 if(this.container) { 1894 this.container.option('value', this.value); 1895 } 1896 $.when(this.render(response)) 1897 .then($.proxy(function() { 1898 this.handleEmpty(); 1899 }, this)); 1900 }, 1901 1902 /** 1903 Activates input of visible container (e.g. set focus) 1904 @method activate() 1905 **/ 1906 activate: function() { 1907 if(this.container) { 1908 this.container.activate(); 1909 } 1910 }, 1911 1912 /** 1913 Removes editable feature from element 1914 @method destroy() 1915 **/ 1916 destroy: function() { 1917 this.disable(); 1918 1919 if(this.container) { 1920 this.container.destroy(); 1921 } 1922 1923 this.input.destroy(); 1924 1925 if(this.options.toggle !== 'manual') { 1926 this.$element.removeClass('editable-click'); 1927 this.$element.off(this.options.toggle + '.editable'); 1928 } 1929 1930 this.$element.off("save.internal"); 1931 1932 this.$element.removeClass('editable editable-open editable-disabled'); 1933 this.$element.removeData('editable'); 1934 } 1935 }; 1936 1937 /* EDITABLE PLUGIN DEFINITION 1938 * ======================= */ 1939 1940 /** 1941 jQuery method to initialize editable element. 1942 1943 @method $().editable(options) 1944 @params {Object} options 1945 @example 1946 $('#username').editable({ 1947 type: 'text', 1948 url: '/post', 1949 pk: 1 1950 }); 1951 **/ 1952 $.fn.editable = function (option) { 1953 //special API methods returning non-jquery object 1954 var result = {}, args = arguments, datakey = 'editable'; 1955 switch (option) { 1956 /** 1957 Runs client-side validation for all matched editables 1958 1959 @method validate() 1960 @returns {Object} validation errors map 1961 @example 1962 $('#username, #fullname').editable('validate'); 1963 // possible result: 1964 { 1965 username: "username is required", 1966 fullname: "fullname should be minimum 3 letters length" 1967 } 1968 **/ 1969 case 'validate': 1970 this.each(function () { 1971 var $this = $(this), data = $this.data(datakey), error; 1972 if (data && (error = data.validate())) { 1973 result[data.options.name] = error; 1974 } 1975 }); 1976 return result; 1977 1978 /** 1979 Returns current values of editable elements. 1980 Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. 1981 If value of some editable is `null` or `undefined` it is excluded from result object. 1982 When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. 1983 1984 @method getValue() 1985 @param {bool} isSingle whether to return just value of single element 1986 @returns {Object} object of element names and values 1987 @example 1988 $('#username, #fullname').editable('getValue'); 1989 //result: 1990 { 1991 username: "superuser", 1992 fullname: "John" 1993 } 1994 //isSingle = true 1995 $('#username').editable('getValue', true); 1996 //result "superuser" 1997 **/ 1998 case 'getValue': 1999 if(arguments.length === 2 && arguments[1] === true) { //isSingle = true 2000 result = this.eq(0).data(datakey).value; 2001 } else { 2002 this.each(function () { 2003 var $this = $(this), data = $this.data(datakey); 2004 if (data && data.value !== undefined && data.value !== null) { 2005 result[data.options.name] = data.input.value2submit(data.value); 2006 } 2007 }); 2008 } 2009 return result; 2010 2011 /** 2012 This method collects values from several editable elements and submit them all to server. 2013 Internally it runs client-side validation for all fields and submits only in case of success. 2014 See <a href="#newrecord">creating new records</a> for details. 2015 2016 @method submit(options) 2017 @param {object} options 2018 @param {object} options.url url to submit data 2019 @param {object} options.data additional data to submit 2020 @param {object} options.ajaxOptions additional ajax options 2021 @param {function} options.error(obj) error handler 2022 @param {function} options.success(obj,config) success handler 2023 @returns {Object} jQuery object 2024 **/ 2025 case 'submit': //collects value, validate and submit to server for creating new record 2026 var config = arguments[1] || {}, 2027 $elems = this, 2028 errors = this.editable('validate'), 2029 values; 2030 2031 if($.isEmptyObject(errors)) { 2032 values = this.editable('getValue'); 2033 if(config.data) { 2034 $.extend(values, config.data); 2035 } 2036 2037 $.ajax($.extend({ 2038 url: config.url, 2039 data: values, 2040 type: 'POST' 2041 }, config.ajaxOptions)) 2042 .success(function(response) { 2043 //successful response 200 OK 2044 if(typeof config.success === 'function') { 2045 config.success.call($elems, response, config); 2046 } 2047 }) 2048 .error(function(){ //ajax error 2049 if(typeof config.error === 'function') { 2050 config.error.apply($elems, arguments); 2051 } 2052 }); 2053 } else { //client-side validation error 2054 if(typeof config.error === 'function') { 2055 config.error.call($elems, errors); 2056 } 2057 } 2058 return this; 2059 } 2060 2061 //return jquery object 2062 return this.each(function () { 2063 var $this = $(this), 2064 data = $this.data(datakey), 2065 options = typeof option === 'object' && option; 2066 2067 //for delegated targets do not store `editable` object for element 2068 //it's allows several different selectors. 2069 //see: https://github.com/vitalets/x-editable/issues/312 2070 if(options && options.selector) { 2071 data = new Editable(this, options); 2072 return; 2073 } 2074 2075 if (!data) { 2076 $this.data(datakey, (data = new Editable(this, options))); 2077 } 2078 2079 if (typeof option === 'string') { //call method 2080 data[option].apply(data, Array.prototype.slice.call(args, 1)); 2081 } 2082 }); 2083 }; 2084 2085 2086 $.fn.editable.defaults = { 2087 /** 2088 Type of input. Can be <code>
2088text|textarea|select|date|checklist</code> and more 2089 2090 @property type 2091 @type string 2092 @default 'text' 2093 **/ 2094 type: 'text', 2095 /** 2096 Sets disabled state of editable 2097 2098 @property disabled 2099 @type boolean 2100 @default false 2101 **/ 2102 disabled: false, 2103 /** 2104 How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>. 2105 When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable. 2106 **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element, 2107 you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document. 2108 2109 @example 2110 $('#edit-button').click(function(e) { 2111 e.stopPropagation(); 2112 $('#username').editable('toggle'); 2113 }); 2114 2115 @property toggle 2116 @type string 2117 @default 'click' 2118 **/ 2119 toggle: 'click', 2120 /** 2121 Text shown when element is empty. 2122 2123 @property emptytext 2124 @type string 2125 @default 'Empty' 2126 **/ 2127 emptytext: 'Empty', 2128 /** 2129 Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date. 2130 For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>. 2131 <code>auto</code> - text will be automatically set only if element is empty. 2132 <code>always|never</code> - always(never) try to set element's text. 2133 2134 @property autotext 2135 @type string 2136 @default 'auto' 2137 **/ 2138 autotext: 'auto', 2139 /** 2140 Initial value of input. If not set, taken from element's text. 2141 Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option). 2142 For example, to display currency sign: 2143 @example 2144 <a id="price" data-type="text" data-value="100"></a> 2145 <script> 2146 $('#price').editable({ 2147 ... 2148 display: function(value) { 2149 $(this).text(value + '$'); 2150 } 2151 }) 2152 </script> 2153 2154 @property value 2155 @type mixed 2156 @default element's text 2157 **/ 2158 value: null, 2159 /** 2160 Callback to perform custom displaying of value in element's text.
2161 If `null`, default input's display used. 2162 If `false`, no displaying methods will be called, element's text will never change. 2163 Runs under element's scope. 2164 _**Parameters:**_ 2165 2166 * `value` current value to be displayed 2167 * `response` server response (if display called after ajax submit), since 1.4.0 2168 2169 For _inputs with source_ (select, checklist) parameters are different: 2170 2171 * `value` current value to be displayed 2172 * `sourceData` array of items for current input (e.g. dropdown items) 2173 * `response` server response (if display called after ajax submit), since 1.4.0 2174 2175 To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`. 2176 2177 @property display 2178 @type function|boolean 2179 @default null 2180 @since 1.2.0 2181 @example 2182 display: function(value, sourceData) { 2183 //display checklist as comma-separated values 2184 var html = [], 2185 checked = $.fn.editableutils.itemsByValue(value, sourceData); 2186 2187 if(checked.length) { 2188 $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); 2189 $(this).html(html.join(', ')); 2190 } else { 2191 $(this).empty(); 2192 } 2193 } 2194 **/ 2195 display: null, 2196 /** 2197 Css class applied when editable text is empty. 2198 2199 @property emptyclass 2200 @type string 2201 @since 1.4.1 2202 @default editable-empty 2203 **/ 2204 emptyclass: 'editable-empty', 2205 /** 2206 Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). 2207 You may set it to `null` if you work with editables locally and submit them together. 2208 2209 @property unsavedclass 2210 @type string 2211 @since 1.4.1 2212 @default editable-unsaved 2213 **/ 2214 unsavedclass: 'editable-unsaved', 2215 /** 2216 If selector is provided, editable will be delegated to the specified targets. 2217 Usefull for dynamically generated DOM elements. 2218 **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options, 2219 as they actually become editable only after first click. 2220 You should manually set class `editable-click` to these elements. 2221 Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element: 2222 2223 @property selector 2224 @type string 2225 @since 1.4.1 2226 @default null 2227 @example 2228 <div id="user"> 2229 <!-- empty --> 2230 <a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a> 2231 <!-- non-empty --> 2232 <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a> 2233 </div> 2234 2235 <script> 2236 $('#user').editable({ 2237 selector: 'a', 2238 url: '/post', 2239 pk: 1 2240 }); 2241 </script> 2242 **/ 2243 selector: null, 2244 /** 2245 Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. 2246 2247 @property highlight 2248 @type string|boolean 2249 @since 1.4.5 2250 @default #FFFF80 2251 **/ 2252 highlight: '#FFFF80' 2253 }; 2254 2255}(window.jQuery)); 2256 2257/** 2258AbstractInput - base class for all editable inputs. 2259It defines interface to be implemented by any input type. 2260To create your own input you can inherit from this class. 2261 2262@class abstractinput 2263**/ 2264(function ($) { 2265 "use strict"; 2266 2267 //types 2268 $.fn.editabletypes = {}; 2269 2270 var AbstractInput = function () { }; 2271 2272 AbstractInput.prototype = { 2273 /** 2274 Initializes input 2275 2276 @method init() 2277 **/ 2278 init: function(type, options, defaults) { 2279 this.type = type; 2280 this.options = $.extend({}, defaults, options); 2281 }, 2282 2283 /* 2284 this method called before render to init $tpl that is inserted in DOM 2285 */ 2286 prerender: function() { 2287 this.$tpl = $(this.options.tpl); //whole tpl as jquery object 2288 this.$input = this.$tpl; //control itself, can be changed in render method 2289 this.$clear = null; //clear button 2290 this.error = null; //error message, if input cannot be rendered 2291 }, 2292 2293 /** 2294 Renders input from tpl. Can return jQuery deferred object.
2295 Can be overwritten in child objects 2296 2297 @method render() 2298 **/ 2299 render: function() { 2300 2301 }, 2302 2303 /** 2304 Sets element's html by value. 2305 2306 @method value2html(value, element) 2307 @param {mixed} value 2308 @param {DOMElement} element 2309 **/ 2310 value2html: function(value, element) { 2311 $(element).text($.trim(value)); 2312 }, 2313 2314 /** 2315 Converts element's html to value 2316 2317 @method html2value(html) 2318 @param {string} html 2319 @returns {mixed} 2320 **/ 2321 html2value: function(html) { 2322 return $('<div>').html(html).text(); 2323 }, 2324 2325 /** 2326 Converts value to string (for internal compare). For submitting to server used value2submit(). 2327 2328 @method value2str(value) 2329 @param {mixed} value 2330 @returns {string} 2331 **/ 2332 value2str: function(value) { 2333 return value; 2334 }, 2335 2336 /** 2337 Converts string received from server into value. Usually from `data-value` attribute. 2338 2339 @method str2value(str) 2340 @param {string} str 2341 @returns {mixed} 2342 **/ 2343 str2value: function(str) { 2344 return str; 2345 }, 2346 2347 /** 2348 Converts value for submitting to server. Result can be string or object. 2349 2350 @method value2submit(value) 2351 @param {mixed} value 2352 @returns {mixed} 2353 **/ 2354 value2submit: function(value) { 2355 return value; 2356 }, 2357 2358 /** 2359 Sets value of input. 2360 2361 @method value2input(value) 2362 @param {mixed} value 2363 **/ 2364 value2input: function(value) { 2365 this.$input.val(value); 2366 }, 2367 2368 /** 2369 Returns value of input. Value can be object (e.g. datepicker) 2370 2371 @method input2value() 2372 **/ 2373 input2value: function() { 2374 return this.$input.val(); 2375 }, 2376 2377 /** 2378 Activates input. For text it sets focus. 2379 2380 @method activate() 2381 **/ 2382 activate: function() { 2383 if(this.$input.is(':visible')) { 2384 this.$input.focus(); 2385 } 2386 }, 2387 2388 /** 2389 Creates input. 2390 2391 @method clear() 2392 **/ 2393 clear: function() { 2394 this.$input.val(null); 2395 }, 2396 2397 /** 2398 method to escape html. 2399 **/ 2400 escape: function(str) { 2401 return $('<div>').text(str).html(); 2402 }, 2403 2404 /** 2405 attach handler to automatically submit form when value changed (useful when buttons not shown) 2406 **/ 2407 autosubmit: function() { 2408 2409 }, 2410 2411 /** 2412 Additional actions when destroying element 2413 **/ 2414 destroy: function() { 2415 }, 2416 2417 // -------- helper functions -------- 2418 setClass: function() { 2419 if(this.options.inputclass) { 2420 this.$input.addClass(this.options.inputclass); 2421 } 2422 }, 2423 2424 setAttr: function(attr) { 2425 if (this.options[attr] !== undefined && this.options[attr] !== null) { 2426 this.$input.attr(attr, this.options[attr]); 2427 } 2428 }, 2429 2430 option: function(key, value) { 2431 this.options[key] = value; 2432 } 2433 2434 }; 2435 2436 AbstractInput.defaults = { 2437 /** 2438 HTML template of input. Normally you should not change it. 2439 2440 @property tpl 2441 @type string 2442 @default '' 2443 **/ 2444 tpl: '', 2445 /** 2446 CSS class automatically applied to input 2447 2448 @property inputclass 2449 @type string 2450 @default input-medium 2451 **/ 2452 inputclass: 'input-medium', 2453 //scope for external methods (e.g. source defined as function) 2454 //for internal use only 2455 scope: null, 2456 2457 //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) 2458 showbuttons: true 2459 }; 2460 2461 $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); 2462 2463}(window.jQuery)); 2464 2465/** 2466List - abstract class for inputs that have source option loaded from js array or via ajax 2467 2468@class list 2469@extends abstractinput 2470**/ 2471(function ($) { 2472 "use strict"; 2473 2474 var List = function (options) { 2475 2476 }; 2477 2478 $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); 2479 2480 $.extend(List.prototype, { 2481 render: function () { 2482 var deferred = $.Deferred(); 2483 2484 this.error = null; 2485 this.onSourceReady(function () { 2486 this.renderList(); 2487 deferred.resolve(); 2488 }, function () { 2489 this.error = this.options.sourceError; 2490 deferred.resolve(); 2491 }); 2492 2493 return deferred.promise(); 2494 }, 2495 2496 html2value: function (html) { 2497 return null; //can't set value by text 2498 }, 2499 2500 value2html: function (value, element, display, response) { 2501 var deferred = $.Deferred(), 2502 success = function () { 2503 if(typeof display === 'function') { 2504 //custom display method 2505 display.call(element, value, this.sourceData, response); 2506 } else { 2507 this.value2htmlFinal(value, element); 2508 } 2509 deferred.resolve(); 2510 }; 2511 2512 //for null value just call success without loading source 2513 if(value === null) { 2514 success.call(this); 2515 } else { 2516 this.onSourceReady(success, function () { deferred.resolve(); }); 2517 } 2518 2519 return deferred.promise(); 2520 }, 2521 2522 // ------------- additional functions ------------ 2523 2524 onSourceReady: function (success, error) { 2525 //run source if it function 2526 var source; 2527 if ($.isFunction(this.options.source)) { 2528 source = this.options.source.call(this.options.scope); 2529 this.sourceData = null; 2530 //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed 2531 } else { 2532 source = this.options.source; 2533 } 2534 2535 //if allready loaded just call success 2536 if(this.options.sourceCache && $.isArray(this.sourceData)) { 2537 success.call(this); 2538 return; 2539 } 2540 2541 //try parse json in single quotes (for double quotes jquery does automatically) 2542 try { 2543 source = $.fn.editableutils.tryParseJson(source, false); 2544 } catch (e) { 2545 error.call(this); 2546 return; 2547 } 2548 2549 //loading from url 2550 if (typeof source === 'string') { 2551 //try to get sourceData from cache 2552 if(this.options.sourceCache) { 2553 var cacheID = source, 2554 cache; 2555 2556 if (!$(document).data(cacheID)) { 2557 $(document).data(cacheID, {}); 2558 } 2559 cache = $(document).data(cacheID); 2560 2561 //check for cached data 2562 if (cache.loading === false && cache.sourceData) { //take source from cache 2563 this.sourceData = cache.sourceData; 2564 this.doPrepend(); 2565 success.call(this); 2566 return; 2567 } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later 2568 cache.callbacks.push($.proxy(function () { 2569 this.sourceData = cache.sourceData; 2570 this.doPrepend(); 2571 success.call(this); 2572 }, this)); 2573 2574 //also collecting error callbacks 2575 cache.err_callbacks.push($.proxy(error, this)); 2576 return; 2577 } else { //no cache yet, activate it 2578 cache.loading = true; 2579 cache.callbacks = []; 2580 cache.err_callbacks = []; 2581 } 2582 } 2583 2584 //loading sourceData from server 2585 $.ajax({ 2586 url: source, 2587 type: 'get', 2588 cache: false, 2589 dataType: 'json', 2590 success: $.proxy(function (data) { 2591 if(cache) { 2592 cache.loading = false;
2593 } 2594 this.sourceData = this.makeArray(data); 2595 if($.isArray(this.sourceData)) { 2596 if(cache) { 2597 //store result in cache 2598 cache.sourceData = this.sourceData; 2599 //run success callbacks for other fields waiting for this source 2600 $.each(cache.callbacks, function () { this.call(); }); 2601 } 2602 this.doPrepend(); 2603 success.call(this); 2604 } else { 2605 error.call(this); 2606 if(cache) { 2607 //run error callbacks for other fields waiting for this source 2608 $.each(cache.err_callbacks, function () { this.call(); }); 2609 } 2610 } 2611 }, this), 2612 error: $.proxy(function () { 2613 error.call(this); 2614 if(cache) { 2615 cache.loading = false; 2616 //run error callbacks for other fields 2617 $.each(cache.err_callbacks, function () { this.call(); }); 2618 } 2619 }, this) 2620 }); 2621 } else { //options as json/array 2622 this.sourceData = this.makeArray(source); 2623 2624 if($.isArray(this.sourceData)) { 2625 this.doPrepend(); 2626 success.call(this); 2627 } else { 2628 error.call(this); 2629 } 2630 } 2631 }, 2632 2633 doPrepend: function () { 2634 if(this.options.prepend === null || this.options.prepend === undefined) { 2635 return; 2636 } 2637 2638 if(!$.isArray(this.prependData)) { 2639 //run prepend if it is function (once) 2640 if ($.isFunction(this.options.prepend)) { 2641 this.options.prepend = this.options.prepend.call(this.options.scope); 2642 } 2643 2644 //try parse json in single quotes 2645 this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); 2646 2647 //convert prepend from string to object 2648 if (typeof this.options.prepend === 'string') { 2649 this.options.prepend = {'': this.options.prepend}; 2650 } 2651 2652 this.prependData = this.makeArray(this.options.prepend); 2653 } 2654 2655 if($.isArray(this.prependData) && $.isArray(this.sourceData)) { 2656 this.sourceData = this.prependData.concat(this.sourceData); 2657 } 2658 }, 2659 2660 /* 2661 renders input list 2662 */ 2663 renderList: function() { 2664 // this method should be overwritten in child class 2665 }, 2666 2667 /* 2668 set element's html by value 2669 */ 2670 value2htmlFinal: function(value, element) { 2671 // this method should be overwritten in child class 2672 }, 2673 2674 /** 2675 * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] 2676 */ 2677 makeArray: function(data) { 2678 var count, obj, result = [], item, iterateItem; 2679 if(!data || typeof data === 'string') { 2680 return null; 2681 } 2682 2683 if($.isArray(data)) { //array 2684 /* 2685 function to iterate inside item of array if item is object. 2686 Caclulates count of keys in item and store in obj. 2687 */ 2688 iterateItem = function (k, v) { 2689 obj = {value: k, text: v}; 2690 if(count++ >= 2) { 2691 return false;// exit from `each` if item has more than one key. 2692 } 2693 }; 2694 2695 for(var i = 0; i < data.length; i++) { 2696 item = data[i]; 2697 if(typeof item === 'object') { 2698 count = 0; //count of keys inside item 2699 $.each(item, iterateItem); 2700 //case: [{val1: 'text1'}, {val2: 'text2} ...] 2701 if(count === 1) { 2702 result.push(obj); 2703 //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] 2704 } else if(count > 1) { 2705 //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') 2706 if(item.children) { 2707 item.children = this.makeArray(item.children); 2708 } 2709 result.push(item); 2710 } 2711 } else { 2712 //case: ['text1', 'text2' ...] 2713 result.push({value: item, text: item}); 2714 } 2715 } 2716 } else { //case: {val1: 'text1', val2: 'text2, ...} 2717 $.each(data, function (k, v) { 2718 result.push({value: k, text: v}); 2719 }); 2720 } 2721 return result; 2722 }, 2723 2724 option: function(key, value) { 2725 this.options[key] = value; 2726 if(key === 'source') { 2727 this.sourceData = null; 2728 } 2729 if(key === 'prepend') { 2730 this.prependData = null; 2731 } 2732 } 2733 2734 }); 2735 2736 List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 2737 /** 2738 Source data for list. 2739 If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]` 2740 For compability, object format is also supported: `{"1": "text1", "2"
2740: "text2" ...}` but it does not guarantee elements order. 2741 2742 If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option. 2743 2744 If **function**, it should return data in format above (since 1.4.0). 2745 2746 Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only). 2747 `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]` 2748 2749 2750 @property source 2751 @type string | array | object | function 2752 @default null 2753 **/ 2754 source: null, 2755 /** 2756 Data automatically prepended to the beginning of dropdown list. 2757 2758 @property prepend 2759 @type string | array | object | function 2760 @default false 2761 **/ 2762 prepend: false, 2763 /** 2764 Error message when list cannot be loaded (e.g. ajax error) 2765 2766 @property sourceError 2767 @type string 2768 @default Error when loading list 2769 **/ 2770 sourceError: 'Error when loading list', 2771 /** 2772 if <code>true</code> and source is **string url** - results will be cached for fields with the same source. 2773 Usefull for editable column in grid to prevent extra requests. 2774 2775 @property sourceCache 2776 @type boolean 2777 @default true 2778 @since 1.2.0 2779 **/ 2780 sourceCache: true 2781 }); 2782 2783 $.fn.editabletypes.list = List; 2784 2785}(window.jQuery)); 2786 2787/** 2788Text input 2789 2790@class text 2791@extends abstractinput 2792@final 2793@example 2794<a href="#" id="username" data-type="text" data-pk="1">awesome</a> 2795<script> 2796$(function(){ 2797 $('#username').editable({ 2798 url: '/post', 2799 title: 'Enter username' 2800 }); 2801}); 2802</script> 2803**/ 2804(function ($) { 2805 "use strict"; 2806 2807 var Text = function (options) { 2808 this.init('text', options, Text.defaults); 2809 }; 2810 2811 $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); 2812 2813 $.extend(Text.prototype, { 2814 render: function() { 2815 this.renderClear(); 2816 this.setClass(); 2817 this.setAttr('placeholder'); 2818 }, 2819 2820 activate: function() { 2821 if(this.$input.is(':visible')) { 2822 this.$input.focus(); 2823 $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); 2824 if(this.toggleClear) { 2825 this.toggleClear(); 2826 } 2827 } 2828 }, 2829 2830 //render clear button 2831 renderClear: function() { 2832 if (this.options.clear) { 2833 this.$clear = $('<span class="editable-clear-x"></span>'); 2834 this.$input.after(this.$clear) 2835 .css('padding-right', 24) 2836 .keyup($.proxy(function(e) { 2837 //arrows, enter, tab, etc 2838 if(~$.inArray(e.keyCode, [40,38,9,13,27])) { 2839 return; 2840 } 2841 2842 clearTimeout(this.t); 2843 var that = this; 2844 this.t = setTimeout(function() { 2845 that.toggleClear(e); 2846 }, 100); 2847 2848 }, this)) 2849 .parent().css('position', 'relative'); 2850 2851 this.$clear.click($.proxy(this.clear, this)); 2852 } 2853 }, 2854 2855 postrender: function() { 2856 /* 2857 //now `clear` is positioned via css 2858 if(this.$clear) { 2859 //can position clear button only here, when form is shown and height can be calculated 2860// var h = this.$input.outerHeight(true) || 20, 2861 var h = this.$clear.parent().height(), 2862 delta = (h - this.$clear.height()) / 2; 2863 2864 //this.$clear.css({bottom: delta, right: delta}); 2865 } 2866 */ 2867 }, 2868 2869 //show / hide clear button 2870 toggleClear: function(e) { 2871 if(!this.$clear) { 2872 return; 2873 } 2874 2875 var len = this.$input.val().length, 2876 visible = this.$clear.is(':visible'); 2877 2878 if(len && !visible) { 2879 this.$clear.show(); 2880 } 2881 2882 if(!len && visible) { 2883 this.$clear.hide(); 2884 } 2885 }, 2886 2887 clear: function() { 2888 this.$clear.hide(); 2889 this.$input.val('').focus(); 2890 } 2891 }); 2892 2893 Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 2894 /** 2895 @property tpl 2896 @default <input type="text"> 2897 **/ 2898 tpl: '<input type="text">', 2899 /** 2900 Placeholder attribute of input. Shown when input is empty. 2901 2902 @property placeholder 2903 @type string 2904 @default null 2905 **/ 2906 placeholder: null, 2907 2908 /** 2909 Whether to show `clear` button 2910 2911 @property clear 2912 @type boolean 2913 @default true 2914 **/ 2915 clear: true 2916 }); 2917 2918 $.fn.editabletypes.text = Text; 2919 2920}(window.jQuery)); 2921 2922/** 2923Textarea input 2924 2925@class textarea 2926@extends abstractinput 2927@final 2928@example 2929<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a> 2930<script> 2931$(function(){ 2932 $('#comments').editable({ 2933 url: '/post', 2934 title: 'Enter comments', 2935 rows: 10 2936 }); 2937}); 2938</script> 2939**/ 2940(function ($) { 2941 "use strict"; 2942 2943 var Textarea = function (options) { 2944 this.init('textarea', options, Textarea.defaults); 2945 }; 2946 2947 $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); 2948 2949 $.extend(Textarea.prototype, { 2950 render: function () { 2951 this.setClass(); 2952 this.setAttr('placeholder'); 2953 this.setAttr('rows'); 2954 2955 //ctrl + enter
2956 this.$input.keydown(function (e) { 2957 if (e.ctrlKey && e.which === 13) { 2958 $(this).closest('form').submit(); 2959 } 2960 }); 2961 }, 2962 2963 //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant! 2964 /* 2965 value2html: function(value, element) { 2966 var html = '', lines; 2967 if(value) { 2968 lines = value.split("\n"); 2969 for (var i = 0; i < lines.length; i++) { 2970 lines[i] = $('<div>').text(lines[i]).html(); 2971 } 2972 html = lines.join('<br>'); 2973 } 2974 $(element).html(html); 2975 }, 2976 2977 html2value: function(html) { 2978 if(!html) { 2979 return ''; 2980 } 2981 2982 var regex = new RegExp(String.fromCharCode(10), 'g'); 2983 var lines = html.split(/<br\s*\/?>/i); 2984 for (var i = 0; i < lines.length; i++) { 2985 var text = $('<div>').html(lines[i]).text(); 2986 2987 // Remove newline characters (\n) to avoid them being converted by value2html() method 2988 // thus adding extra <br> tags 2989 text = text.replace(regex, ''); 2990 2991 lines[i] = text; 2992 } 2993 return lines.join("\n"); 2994 }, 2995 */ 2996 activate: function() { 2997 $.fn.editabletypes.text.prototype.activate.call(this); 2998 } 2999 }); 3000 3001 Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3002 /** 3003 @property tpl 3004 @default <textarea></textarea> 3005 **/ 3006 tpl:'<textarea></textarea>', 3007 /** 3008 @property inputclass 3009 @default input-large 3010 **/ 3011 inputclass: 'input-large', 3012 /** 3013 Placeholder attribute of input. Shown when input is empty. 3014 3015 @property placeholder 3016 @type string 3017 @default null 3018 **/ 3019 placeholder: null, 3020 /** 3021 Number of rows in textarea 3022 3023 @property rows 3024 @type integer 3025 @default 7 3026 **/ 3027 rows: 7 3028 }); 3029 3030 $.fn.editabletypes.textarea = Textarea; 3031 3032}(window.jQuery)); 3033 3034/** 3035Select (dropdown) 3036 3037@class select 3038@extends list 3039@final 3040@example 3041<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a> 3042<script> 3043$(function(){ 3044 $('#status').editable({ 3045 value: 2, 3046 source: [ 3047 {value: 1, text: 'Active'}, 3048 {value: 2, text: 'Blocked'}, 3049 {value: 3, text: 'Deleted'} 3050 ] 3051 }); 3052}); 3053</script> 3054**/ 3055(function ($) { 3056 "use strict"; 3057 3058 var Select = function (options) { 3059 this.init('select', options, Select.defaults); 3060 }; 3061 3062 $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); 3063 3064 $.extend(Select.prototype, { 3065 renderList: function() { 3066 this.$input.empty(); 3067 3068 var fillItems = function($el, data) { 3069 var attr; 3070 if($.isArray(data)) { 3071 for(var i=0; i<data.length; i++) { 3072 attr = {}; 3073 if(data[i].children) { 3074 attr.label = data[i].text; 3075 $el.append(fillItems($('<optgroup>', attr), data[i].children)); 3076 } else { 3077 attr.value = data[i].value; 3078 if(data[i].disabled) { 3079 attr.disabled = true; 3080 } 3081 $el.append($('<option>', attr).text(data[i].text)); 3082 } 3083 } 3084 } 3085 return $el; 3086 }; 3087 3088 fillItems(this.$input, this.sourceData); 3089 3090 this.setClass(); 3091 3092 //enter submit 3093 this.$input.on('keydown.editable', function (e) { 3094 if (e.which === 13) { 3095 $(this).closest('form').submit(); 3096 } 3097 }); 3098 }, 3099 3100 value2htmlFinal: function(value, element) { 3101 var text = '', 3102 items = $.fn.editableutils.itemsByValue(value, this.sourceData); 3103 3104 if(items.length) { 3105 text = items[0].text; 3106 } 3107 3108 $(element).text(text); 3109 }, 3110 3111 autosubmit: function() { 3112 this.$input.off('keydown.editable').on('change.editable', function(){ 3113 $(this).closest('form').submit(); 3114 }); 3115 } 3116 }); 3117 3118 Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { 3119 /** 3120 @property tpl 3121 @default <select></select> 3122 **/ 3123 tpl:'<select></select>' 3124 }); 3125 3126 $.fn.editabletypes.select = Select; 3127 3128}(window.jQuery)); 3129 3130/** 3131List of checkboxes. 3132Internally value stored as javascript array of values. 3133 3134@class checklist 3135@extends list 3136@final 3137@example 3138<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a> 3139<script> 3140$(function(){ 3141 $('#options').editable({ 3142 value: [2, 3], 3143 source: [ 3144 {value: 1, text: 'option1'}, 3145 {value: 2, text: 'option2'}, 3146 {value: 3, text: 'option3'} 3147 ] 3148 }); 3149}); 3150</script> 3151**/ 3152(function ($) { 3153 "use strict"; 3154 3155 var Checklist = function (options) { 3156 this.init('checklist', options, Checklist.defaults); 3157 }; 3158 3159 $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); 3160 3161 $.extend(Checklist.prototype, { 3162 renderList: function() { 3163 var $label, $div; 3164 3165 this.$tpl.empty(); 3166 3167 if(!$.isArray(this.sourceData)) { 3168 return; 3169 } 3170 3171 for(var i=0; i<this.sourceData.length; i++) { 3172 $label = $('<label>').append($('<input>', { 3173 type: 'checkbox', 3174 value: this.sourceData[i].value 3175 })) 3176 .append($('<span>').text(' '+this.sourceData[i].text)); 3177 3178 $('<div>').append($label).appendTo(this.$tpl); 3179 } 3180
3181 this.$input = this.$tpl.find('input[type="checkbox"]'); 3182 this.setClass(); 3183 }, 3184 3185 value2str: function(value) { 3186 return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; 3187 }, 3188 3189 //parse separated string 3190 str2value: function(str) { 3191 var reg, value = null; 3192 if(typeof str === 'string' && str.length) { 3193 reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*'); 3194 value = str.split(reg); 3195 } else if($.isArray(str)) { 3196 value = str; 3197 } else { 3198 value = [str]; 3199 } 3200 return value; 3201 }, 3202 3203 //set checked on required checkboxes 3204 value2input: function(value) { 3205 this.$input.prop('checked', false); 3206 if($.isArray(value) && value.length) { 3207 this.$input.each(function(i, el) { 3208 var $el = $(el); 3209 // cannot use $.inArray as it performs strict comparison 3210 $.each(value, function(j, val){ 3211 /*jslint eqeq: true*/ 3212 if($el.val() == val) { 3213 /*jslint eqeq: false*/ 3214 $el.prop('checked', true); 3215 } 3216 }); 3217 }); 3218 } 3219 }, 3220 3221 input2value: function() { 3222 var checked = []; 3223 this.$input.filter(':checked').each(function(i, el) { 3224 checked.push($(el).val()); 3225 }); 3226 return checked; 3227 }, 3228 3229 //collect text of checked boxes 3230 value2htmlFinal: function(value, element) { 3231 var html = [], 3232 checked = $.fn.editableutils.itemsByValue(value, this.sourceData); 3233 3234 if(checked.length) { 3235 $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); 3236 $(element).html(html.join('<br>')); 3237 } else { 3238 $(element).empty(); 3239 } 3240 }, 3241 3242 activate: function() { 3243 this.$input.first().focus(); 3244 }, 3245 3246 autosubmit: function() { 3247 this.$input.on('keydown', function(e){ 3248 if (e.which === 13) { 3249 $(this).closest('form').submit(); 3250 } 3251 }); 3252 } 3253 }); 3254 3255 Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { 3256 /** 3257 @property tpl 3258 @default <div></div> 3259 **/ 3260 tpl:'<div class="editable-checklist"></div>', 3261 3262 /** 3263 @property inputclass 3264 @type string 3265 @default null 3266 **/ 3267 inputclass: null, 3268 3269 /** 3270 Separator of values when reading from `data-value` attribute 3271 3272 @property separator 3273 @type string 3274 @default ',' 3275 **/ 3276 separator: ',' 3277 }); 3278 3279 $.fn.editabletypes.checklist = Checklist; 3280 3281}(window.jQuery)); 3282 3283/** 3284HTML5 input types. 3285Following types are supported: 3286 3287* password 3288* email 3289* url 3290* tel 3291* number 3292* range 3293* time 3294 3295Learn more about html5 inputs: 3296http://www.w3.org/wiki/HTML5_form_additions 3297To check browser compatibility please see: 3298https://developer.mozilla.org/en-US/docs/HTML/Element/Input 3299 3300@class html5types 3301@extends text 3302@final 3303@since 1.3.0 3304@example 3305<a href="#" id="email" data-type="email" data-pk="1">[email protected]</a> 3306<script> 3307$(function(){ 3308 $('#email').editable({ 3309 url: '/post', 3310 title: 'Enter email' 3311 }); 3312}); 3313</script> 3314**/ 3315 3316/** 3317@property tpl 3318@default depends on type 3319**/ 3320 3321/* 3322Password 3323*/ 3324(function ($) { 3325 "use strict"; 3326 3327 var Password = function (options) { 3328 this.init('password', options, Password.defaults); 3329 }; 3330 $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); 3331 $.extend(Password.prototype, { 3332 //do not display password, show '[hidden]' instead 3333 value2html: function(value, element) { 3334 if(value) { 3335 $(element).text('[hidden]'); 3336 } else { 3337 $(element).empty(); 3338 } 3339 }, 3340 //as password not displayed, should not set value by html 3341 html2value: function(html) { 3342 return null; 3343 } 3344 }); 3345 Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3346 tpl: '<input type="password">' 3347 }); 3348 $.fn.editabletypes.password = Password; 3349}(window.jQuery)); 3350 3351 3352/* 3353Email 3354*/ 3355(function ($) { 3356 "use strict"; 3357 3358 var Email = function (options) { 3359 this.init('email', options, Email.defaults); 3360 }; 3361 $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); 3362 Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3363 tpl: '<input type="email">' 3364 }); 3365 $.fn.editabletypes.email = Email; 3366}(window.jQuery)); 3367 3368 3369/* 3370Url 3371*/ 3372(function ($) { 3373 "use strict"; 3374 3375 var Url = function (options) { 3376 this.init('url', options, Url.defaults); 3377 }; 3378 $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); 3379 Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3380 tpl: '<input type="url">' 3381 }); 3382 $.fn.editabletypes.url = Url; 3383}(window.jQuery)); 3384 3385 3386/* 3387Tel 3388*/ 3389(function ($) { 3390 "use strict"; 3391 3392 var Tel = function (options) { 3393 this.init('tel', options, Tel.defaults); 3394 }; 3395 $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); 3396 Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3397 tpl: '<input type="tel">' 3398 }); 3399 $.fn.editabletypes.tel = Tel; 3400}(window.jQuery)); 3401 3402 3403/* 3404Number 3405*/ 3406(function ($) { 3407 "use strict"; 3408 3409 var NumberInput = function (options) { 3410 this.init('number', options, NumberInput.defaults); 3411 }; 3412 $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); 3413 $.extend(NumberInput.prototype, { 3414 render: function () { 3415 NumberInput.superclass.render.call(this); 3416 this.setAttr('min'); 3417 this.setAttr('max'); 3418 this.setAttr('step'); 3419 }, 3420 postrender: function() { 3421 if(this.$clear) {
3422 //increase right ffset for up/down arrows 3423 this.$clear.css({right: 24}); 3424 /* 3425 //can position clear button only here, when form is shown and height can be calculated 3426 var h = this.$input.outerHeight(true) || 20, 3427 delta = (h - this.$clear.height()) / 2; 3428 3429 //add 12px to offset right for up/down arrows 3430 this.$clear.css({top: delta, right: delta + 16}); 3431 */ 3432 } 3433 } 3434 }); 3435 NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3436 tpl: '<input type="number">', 3437 inputclass: 'input-mini', 3438 min: null, 3439 max: null, 3440 step: null 3441 }); 3442 $.fn.editabletypes.number = NumberInput; 3443}(window.jQuery)); 3444 3445 3446/* 3447Range (inherit from number) 3448*/ 3449(function ($) { 3450 "use strict"; 3451 3452 var Range = function (options) { 3453 this.init('range', options, Range.defaults); 3454 }; 3455 $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); 3456 $.extend(Range.prototype, { 3457 render: function () { 3458 this.$input = this.$tpl.filter('input'); 3459 3460 this.setClass(); 3461 this.setAttr('min'); 3462 this.setAttr('max'); 3463 this.setAttr('step'); 3464 3465 this.$input.on('input', function(){ 3466 $(this).siblings('output').text($(this).val()); 3467 }); 3468 }, 3469 activate: function() { 3470 this.$input.focus(); 3471 } 3472 }); 3473 Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { 3474 tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>', 3475 inputclass: 'input-medium' 3476 }); 3477 $.fn.editabletypes.range = Range; 3478}(window.jQuery)); 3479 3480/* 3481Time 3482*/ 3483(function ($) { 3484 "use strict"; 3485 3486 var Time = function (options) { 3487 this.init('time', options, Time.defaults); 3488 }; 3489 //inherit from abstract, as inheritance from text gives selection error. 3490 $.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput); 3491 $.extend(Time.prototype, { 3492 render: function() { 3493 this.setClass(); 3494 } 3495 }); 3496 Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3497 tpl: '<input type="time">' 3498 }); 3499 $.fn.editabletypes.time = Time; 3500}(window.jQuery)); 3501 3502/** 3503Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. 3504Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. 3505Compatible **select2 version is 3.4.1**! 3506You should manually download and include select2 distributive: 3507 3508 <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> 3509 <script src="select2/select2.js"></script> 3510 3511To make it **bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): 3512 3513 <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link> 3514 3515**Note:** currently `autotext` feature does not work for select2 with `ajax` remote source. 3516You need initially put both `data-value` and element's text youself: 3517 3518 <a href="#" data-type="select2" data-value="1">Text1</a> 3519 3520 3521@class select2 3522@extends abstractinput 3523@since 1.4.1 3524@final 3525@example 3526<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a> 3527<script> 3528$(function(){ 3529 //local source 3530 $('#country').editable({ 3531 source: [ 3532 {id: 'gb', text: 'Great Britain'}, 3533 {id: 'us', text: 'United States'}, 3534 {id: 'ru', text: 'Russia'} 3535 ], 3536 select2: { 3537 multiple: true 3538 } 3539 }); 3540 //remote source (simple) 3541 $('#country').editable({ 3542 source: '/getCountries' 3543 }); 3544 //remote source (advanced) 3545 $('#country').editable({ 3546 select2: { 3547 placeholder: 'Select Country', 3548 allowClear: true, 3549 minimumInputLength: 3, 3550 id: function (item) { 3551 return item.CountryId; 3552 }, 3553 ajax: { 3554 url: '/getCountries', 3555 dataType: 'json', 3556 data: function (term, page) { 3557 return { query: term }; 3558 }, 3559 results: function (data, page) { 3560 return { results: data }; 3561 } 3562 }, 3563 formatResult: function (item) { 3564 return item.CountryName; 3565 }, 3566 formatSelection: function (item) { 3567 return item.CountryName; 3568 }, 3569 initSelection: function (element, callback) { 3570 return $.get('/getCountryById', { query: element.val() }, function (data) { 3571 callback(data); 3572 }); 3573 } 3574 } 3575 }); 3576}); 3577</script> 3578**/ 3579(function ($) { 3580 "use strict"; 3581 3582 var Constructor = function (options) { 3583 this.init('select2', options, Constructor.defaults); 3584 3585 options.select2 = options.select2 || {}; 3586 3587 this.sourceData = null; 3588 3589 //placeholder 3590 if(options.placeholder) { 3591 options.select2.placeholder = options.placeholder; 3592 } 3593 3594 //if not `tags` mode, use source 3595 if(!options.select2.tags && options.source) { 3596 var source = options.source; 3597 //if source is function, call it (once!) 3598 if ($.isFunction(options.source)) { 3599 source = options.source.call(options.scope); 3600 } 3601 3602 if (typeof source === 'string') { 3603 options.select2.ajax = options.select2.ajax || {}; 3604 //some default ajax params 3605 if(!options.select2.ajax.data) { 3606 options.select2.ajax.data = function(term) {return { query:term };}; 3607 } 3608 if(!options.select2.ajax.results) { 3609 options.select2.ajax.results = function(data) { return {results:data };}; 3610 } 3611 options.select2.ajax.url = source; 3612 } else { 3613 //check format and convert x-editable format to select2 format (if needed) 3614 this.sourceData = this.convertSource(source); 3615 options.select2.data = this.sourceData; 3616 } 3617 } 3618 3619 //overriding objects in config (as by default jQuery extend() is not recursive) 3620 this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); 3621 3622 //detect whether it is multi-valued 3623 this.isMultiple = this.options.select2.tags || this.options.select2.multiple; 3624 this.isRemote = ('ajax' in this.options.select2); 3625 3626 //store function returning ID of item 3627 //should be here as used inautotext for local source 3628 this.idFunc = this.options.select2.id; 3629 if (typeof(this.idFunc) !== "function") { 3630 var idKey = this.idFunc || 'id'; 3631 this.idFunc = function (e) { return e[idKey]; }; 3632 } 3633 3634 //store function that renders text in select2 3635 this.formatSelection = this.options.select2.formatSelection; 3636 if (typeof(this.formatSelection) !== "function") { 3637 this.formatSelection = function (e) { return e.text; }; 3638 } 3639 }; 3640 3641 $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); 3642 3643 $.extend(Constructor.prototype, { 3644 render: function() { 3645 this.setClass(); 3646 3647 //apply select2 3648 this.$input.select2(this.options.select2); 3649 3650 //when data is loaded via ajax, we need to know when it's done to populate listData 3651 if(this.isRemote) { 3652 //listen to loaded event to populate data 3653 this.$input.on('select2-loaded', $.proxy(function(e) { 3654 this.sourceData = e.items.results; 3655 }, this)); 3656 } 3657 3658 //trigger resize of editableform to re-position container in multi-valued mode 3659 if(this.isMultiple) { 3660 this.$input.on('change', function() { 3661 $(this).closest('form').parent().triggerHandler('resize'); 3662 }); 3663 } 3664 }, 3665 3666 value2html: function(value, element) { 3667 var text = '', data, 3668 that = this; 3669 3670 if(this.options.select2.tags) { //in tags mode just assign value 3671 data = value; 3672 //data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc); 3673 } else if(this.sourceData) { 3674 data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); 3675 } else { 3676 //can not get list of possible values (e.g. autotext for select2 with ajax source) 3677 } 3678 3679 //data may be array (when multiple values allowed) 3680 if($.isArray(data)) { 3681 //collect selected data and show with separator 3682 text = []; 3683 $.each(data, function(k, v){ 3684 text.push(v && typeof v === 'object' ? that.formatSelection(v) : v); 3685 }); 3686 } else if(data) { 3687 text = that.formatSelection(data); 3688 } 3689 3690 text = $.isArray(text) ? text.join(this.options.viewseparator) : text; 3691 3692 $(element).text(text); 3693 }, 3694 3695 html2value: function(html) { 3696 return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; 3697 }, 3698 3699 value2input: function(value) { 3700 //for local source use data directly from source (to allow autotext) 3701 /* 3702 if(!this.isRemote && !this.isMultiple) { 3703 var items = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); 3704 if(items.length) { 3705 this.$input.select2('data', items[0]); 3706 return; 3707 } 3708 } 3709 */ 3710 3711 //for remote source just set value, text is updated by initSelection 3712 this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) 3713 3714 //if remote source AND no user's initSelection provided --> try to use element's text 3715 if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) { 3716 var customId = this.options.select2.id, 3717 customText = this.options.select2.formatSelection; 3718 if(!customId && !customText) { 3719 var data = {id: value, text: $(this.options.scope).text()}; 3720 this.$input.select2('data', data); 3721 } 3722 } 3723 }, 3724 3725 input2value: function() { 3726 return this.$input.select2('val'); 3727 }, 3728 3729 str2value: function(str, separator) { 3730 if(typeof str !== 'string' || !this.isMultiple) { 3731 return str; 3732 } 3733 3734 separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator; 3735 3736 var val, i, l; 3737 3738 if (str === null || str.length < 1) { 3739 return null; 3740 } 3741 val = str.split(separator); 3742 for (i = 0, l = val.length; i < l; i = i + 1) { 3743 val[i] = $.trim(val[i]); 3744 } 3745 3746 return val; 3747 }, 3748 3749 autosubmit: function() { 3750 this.$input.on('change', function(e, isInitial){ 3751 if(!isInitial) { 3752 $(this).closest('form').submit(); 3753 } 3754 }); 3755 }, 3756 3757 /* 3758 Converts source from x-editable format: {value: 1, text: "1"} to 3759 select2 format: {id: 1, text: "1"} 3760 */ 3761 convertSource: function(source) { 3762 if($.isArray(source) && source.length && source[0].value !== undefined) { 3763 for(var i = 0; i<source.length; i++) { 3764 if(source[i].value !== undefined) { 3765 source[i].id = source[i].value; 3766 delete source[i].value; 3767 } 3768 } 3769 } 3770 return source; 3771 }, 3772 3773 destroy: function() { 3774 if(this.$input.data('select2')) { 3775 this.$input.select2('destroy'); 3776 } 3777 } 3778 3779 }); 3780 3781 Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3782 /** 3783 @property tpl 3784 @default <input type="hidden"> 3785 **/ 3786 tpl:'<input type="hidden">', 3787 /** 3788 Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). 3789 3790 @property select2 3791 @type object 3792 @default null 3793 **/ 3794 select2: null, 3795 /** 3796 Placeholder attribute of select 3797 3798 @property placeholder 3799 @type string 3800 @default null 3801 **/ 3802 placeholder: null, 3803 /** 3804 Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. 3805 Please note, that format is different from simple `select` input: use 'id' instead of 'value'. 3806 E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. 3807 3808 @property source 3809 @type array 3810 @default null 3811 **/ 3812 source: null, 3813 /** 3814 Separator used to display tags. 3815 3816 @property viewseparator 3817 @type string 3818 @default ', ' 3819 **/ 3820 viewseparator: ', ' 3821 }); 3822 3823 $.fn.editabletypes.select2 = Constructor; 3824 3825}(window.jQuery)); 3826 3827/** 3828* Combodate - 1.0.4 3829* Dropdown date and time picker. 3830* Converts text input into dropdowns to pick day, month, year, hour, minute and second. 3831* Uses momentjs as datetime library http://momentjs.com. 3832* For internalization include corresponding file from https://github.com/timrwoo
3832d/moment/tree/master/lang 3833* 3834* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight 3835* In combodate: 3836* 12:00 pm --> 12:00 (24-h format, midday) 3837* 12:00 am --> 00:00 (24-h format, midnight, start of day) 3838* 3839* Differs from momentjs parse rules: 3840* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) 3841* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) 3842* 3843* 3844* Author: Vitaliy Potapov 3845* Project page: http://github.com/vitalets/combodate 3846* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. 3847**/ 3848(function ($) { 3849 3850 var Combodate = function (element, options) { 3851 this.$element = $(element); 3852 if(!this.$element.is('input')) { 3853 $.error('Combodate should be applied to INPUT element'); 3854 return; 3855 } 3856 this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data()); 3857 this.init(); 3858 }; 3859 3860 Combodate.prototype = { 3861 constructor: Combodate, 3862 init: function () { 3863 this.map = { 3864 //key regexp moment.method 3865 day: ['D', 'date'], 3866 month: ['M', 'month'], 3867 year: ['Y', 'year'], 3868 hour: ['[Hh]', 'hours'], 3869 minute: ['m', 'minutes'], 3870 second: ['s', 'seconds'], 3871 ampm: ['[Aa]', ''] 3872 }; 3873 3874 this.$widget = $('<span class="combodate"></span>').html(this.getTemplate()); 3875 3876 this.initCombos(); 3877 3878 //update original input on change 3879 this.$widget.on('change', 'select', $.proxy(function(){ 3880 this.$element.val(this.getValue()); 3881 }, this)); 3882 3883 this.$widget.find('select').css('width', 'auto'); 3884 3885 //hide original input and insert widget 3886 this.$element.hide().after(this.$widget); 3887 3888 //set initial value 3889 this.setValue(this.$element.val() || this.options.value); 3890 }, 3891 3892 /* 3893 Replace tokens in template with <select> elements 3894 */ 3895 getTemplate: function() { 3896 var tpl = this.options.template; 3897 3898 //first pass 3899 $.each(this.map, function(k, v) { 3900 v = v[0]; 3901 var r = new RegExp(v+'+'), 3902 token = v.length > 1 ? v.substring(1, 2) : v; 3903 3904 tpl = tpl.replace(r, '{'+token+'}'); 3905 }); 3906 3907 //replace spaces with 3908 tpl = tpl.replace(/ /g, ' '); 3909 3910 //second pass 3911 $.each(this.map, function(k, v) { 3912 v = v[0]; 3913 var token = v.length > 1 ? v.substring(1, 2) : v; 3914 3915 tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>'); 3916 }); 3917 3918 return tpl; 3919 }, 3920 3921 /* 3922 Initialize combos that presents in template 3923 */ 3924 initCombos: function() { 3925 var that = this; 3926 $.each(this.map, function(k, v) { 3927 var $c = that.$widget.find('.'+k), f, items; 3928 if($c.length) { 3929 that['$'+k] = $c; //set properties like this.$day, this.$month etc. 3930 f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays` 3931 items = that[f](); 3932 that['$'+k].html(that.renderItems(items)); 3933 } 3934 }); 3935 }, 3936 3937 /* 3938 Initialize items of combos. Handles `firstItem` option 3939 */ 3940 initItems: function(key) { 3941 var values = [], 3942 relTime; 3943 3944 if(this.options.firstItem === 'name') { 3945 //need both to support moment ver < 2 and >= 2 3946 relTime = moment.relativeTime || moment.langData()._relativeTime; 3947 var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; 3948 //take last entry (see momentjs lang files structure) 3949 header = header.split(' ').reverse()[0]; 3950 values.push(['', header]); 3951 } else if(this.options.firstItem === 'empty') { 3952 values.push(['', '']); 3953 } 3954 return values; 3955 }, 3956 3957 /* 3958 render items to string of <option> tags 3959 */ 3960 renderItems: function(items) { 3961 var str = []; 3962 for(var i=0; i<items.length; i++) { 3963 str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); 3964 } 3965 return str.join("\n"); 3966 }, 3967 3968 /* 3969 fill day 3970 */ 3971 fillDay: function() { 3972 var items = this.initItems('d'), name, i, 3973 twoDigit = this.options.template.indexOf('DD') !== -1; 3974 3975 for(i=1; i<=31; i++) { 3976 name = twoDigit ? this.leadZero(i) : i; 3977 items.push([i, name]); 3978 } 3979 return items; 3980 }, 3981 3982 /* 3983 fill month 3984 */ 3985 fillMonth: function() { 3986 var items = this.initItems('M'), name, i, 3987 longNames = this.options.template.indexOf('MMMM') !== -1, 3988 shortNames = this.options.template.indexOf('MMM') !== -1, 3989 twoDigit = this.options.template.indexOf('MM') !== -1; 3990 3991 for(i=0; i<=11; i++) { 3992 if(longNames) {
3993 //see https://github.com/timrwood/momentjs.com/pull/36 3994 name = moment().date(1).month(i).format('MMMM'); 3995 } else if(shortNames) { 3996 name = moment().date(1).month(i).format('MMM'); 3997 } else if(twoDigit) { 3998 name = this.leadZero(i+1); 3999 } else { 4000 name = i+1; 4001 } 4002 items.push([i, name]); 4003 } 4004 return items; 4005 }, 4006 4007 /* 4008 fill year 4009 */ 4010 fillYear: function() { 4011 var items = [], name, i, 4012 longNames = this.options.template.indexOf('YYYY') !== -1; 4013 4014 for(i=this.options.maxYear; i>=this.options.minYear; i--) { 4015 name = longNames ? i : (i+'').substring(2); 4016 items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); 4017 } 4018 4019 items = this.initItems('y').concat(items); 4020 4021 return items; 4022 }, 4023 4024 /* 4025 fill hour 4026 */ 4027 fillHour: function() { 4028 var items = this.initItems('h'), name, i, 4029 h12 = this.options.template.indexOf('h') !== -1, 4030 h24 = this.options.template.indexOf('H') !== -1, 4031 twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, 4032 min = h12 ? 1 : 0, 4033 max = h12 ? 12 : 23; 4034 4035 for(i=min; i<=max; i++) { 4036 name = twoDigit ? this.leadZero(i) : i; 4037 items.push([i, name]); 4038 } 4039 return items; 4040 }, 4041 4042 /* 4043 fill minute 4044 */ 4045 fillMinute: function() { 4046 var items = this.initItems('m'), name, i, 4047 twoDigit = this.options.template.indexOf('mm') !== -1; 4048 4049 for(i=0; i<=59; i+= this.options.minuteStep) { 4050 name = twoDigit ? this.leadZero(i) : i; 4051 items.push([i, name]); 4052 } 4053 return items; 4054 }, 4055 4056 /* 4057 fill second 4058 */ 4059 fillSecond: function() { 4060 var items = this.initItems('s'), name, i, 4061 twoDigit = this.options.template.indexOf('ss') !== -1; 4062 4063 for(i=0; i<=59; i+= this.options.secondStep) { 4064 name = twoDigit ? this.leadZero(i) : i; 4065 items.push([i, name]); 4066 } 4067 return items; 4068 }, 4069 4070 /* 4071 fill ampm 4072 */ 4073 fillAmpm: function() { 4074 var ampmL = this.options.template.indexOf('a') !== -1, 4075 ampmU = this.options.template.indexOf('A') !== -1, 4076 items = [ 4077 ['am', ampmL ? 'am' : 'AM'], 4078 ['pm', ampmL ? 'pm' : 'PM'] 4079 ]; 4080 return items; 4081 }, 4082 4083 /* 4084 Returns current date value from combos. 4085 If format not specified - `options.format` used. 4086 If format = `null` - Moment object returned. 4087 */ 4088 getValue: function(format) { 4089 var dt, values = {}, 4090 that = this, 4091 notSelected = false; 4092 4093 //getting selected values 4094 $.each(this.map, function(k, v) { 4095 if(k === 'ampm') { 4096 return; 4097 } 4098 var def = k === 'day' ? 1 : 0; 4099 4100 values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def; 4101 4102 if(isNaN(values[k])) { 4103 notSelected = true; 4104 return false; 4105 } 4106 }); 4107 4108 //if at least one visible combo not selected - return empty string 4109 if(notSelected) { 4110 return ''; 4111 } 4112 4113 //convert hours 12h --> 24h 4114 if(this.$ampm) { 4115 //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) 4116 if(values.hour === 12) { 4117 values.hour = this.$ampm.val() === 'am' ? 0 : 12; 4118 } else { 4119 values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; 4120 } 4121 } 4122 4123 dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); 4124 4125 //highlight invalid date 4126 this.highlight(dt); 4127 4128 format = format === undefined ? this.options.format : format; 4129 if(format === null) { 4130 return dt.isValid() ? dt : null; 4131 } else { 4132 return dt.isValid() ? dt.format(format) : ''; 4133 } 4134 }, 4135 4136 setValue: function(value) { 4137 if(!value) { 4138 return; 4139 } 4140 4141 var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value), 4142 that = this, 4143 values = {}; 4144 4145 //function to find nearest value in select options 4146 function getNearest($select, value) { 4147 var delta = {}; 4148 $select.children('option').each(function(i, opt){ 4149 var optValue = $(opt).attr('value'), 4150 distance; 4151 4152 if(optValue === '') return; 4153 distance = Math.abs(optValue - value); 4154 if(typeof delta.distance === 'undefined' || distance < delta.distance) { 4155 delta = {value: optValue, distance: distance}; 4156 } 4157 }); 4158 return delta.value; 4159 } 4160 4161 if(dt.isValid()) { 4162 //read values from date object 4163 $.each(this.map, function(k, v) { 4164 if(k === 'ampm') { 4165 return; 4166 } 4167 values[k] = dt[v[1]](); 4168 }); 4169 4170 if(this.$ampm) { 4171 //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) 4172 if(values.hour >= 12) { 4173 values.ampm = 'pm'; 4174 if(values.hour > 12) { 4175 values.hour -= 12; 4176 } 4177 } else { 4178 values.ampm = 'am'; 4179 if(values.hour === 0) { 4180 values.hour = 12; 4181 } 4182 } 4183 } 4184 4185 $.each(values, function(k, v) { 4186 //call val() for each existing combo, e.g. this.$hour.val() 4187 if(that['$'+k]) { 4188 4189 if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { 4190 v = getNearest(that['$'+k], v); 4191 } 4192 4193 if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { 4194 v = getNearest(that['$'+k], v); 4195 } 4196 4197 that['$'+k].val(v); 4198 } 4199 }); 4200 4201 this.$element.val(dt.format(this.options.format)); 4202 } 4203 }, 4204 4205 /*
4206 highlight combos if date is invalid 4207 */ 4208 highlight: function(dt) { 4209 if(!dt.isValid()) { 4210 if(this.options.errorClass) { 4211 this.$widget.addClass(this.options.errorClass); 4212 } else { 4213 //store original border color 4214 if(!this.borderColor) { 4215 this.borderColor = this.$widget.find('select').css('border-color'); 4216 } 4217 this.$widget.find('select').css('border-color', 'red'); 4218 } 4219 } else { 4220 if(this.options.errorClass) { 4221 this.$widget.removeClass(this.options.errorClass); 4222 } else { 4223 this.$widget.find('select').css('border-color', this.borderColor); 4224 } 4225 } 4226 }, 4227 4228 leadZero: function(v) { 4229 return v <= 9 ? '0' + v : v; 4230 }, 4231 4232 destroy: function() { 4233 this.$widget.remove(); 4234 this.$element.removeData('combodate').show(); 4235 } 4236 4237 //todo: clear method 4238 }; 4239 4240 $.fn.combodate = function ( option ) { 4241 var d, args = Array.apply(null, arguments); 4242 args.shift(); 4243 4244 //getValue returns date as string / object (not jQuery object) 4245 if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) { 4246 return d.getValue.apply(d, args); 4247 } 4248 4249 return this.each(function () { 4250 var $this = $(this), 4251 data = $this.data('combodate'), 4252 options = typeof option == 'object' && option; 4253 if (!data) { 4254 $this.data('combodate', (data = new Combodate(this, options))); 4255 } 4256 if (typeof option == 'string' && typeof data[option] == 'function') { 4257 data[option].apply(data, args); 4258 } 4259 }); 4260 }; 4261 4262 $.fn.combodate.defaults = { 4263 //in this format value stored in original input 4264 format: 'DD-MM-YYYY HH:mm', 4265 //in this format items in dropdowns are displayed 4266 template: 'D / MMM / YYYY H : mm', 4267 //initial value, can be `new Date()` 4268 value: null, 4269 minYear: 1970, 4270 maxYear: 2015, 4271 yearDescending: true, 4272 minuteStep: 5, 4273 secondStep: 1, 4274 firstItem: 'empty', //'name', 'empty', 'none' 4275 errorClass: null, 4276 roundTime: true //whether to round minutes and seconds if step > 1 4277 }; 4278 4279}(window.jQuery)); 4280/** 4281Combodate input - dropdown date and time picker. 4282Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). 4283 4284 <script src="js/moment.min.js"></script> 4285 4286Allows to input: 4287 4288* only date 4289* only time 4290* both date and time 4291 4292Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker. 4293Internally value stored as `momentjs` object. 4294 4295@class combodate 4296@extends abstractinput 4297@final 4298@since 1.4.0 4299@example 4300<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a> 4301<script> 4302$(function(){ 4303 $('#dob').editable({ 4304 format: 'YYYY-MM-DD', 4305 viewformat: 'DD.MM.YYYY', 4306 template: 'D / MMMM / YYYY', 4307 combodate: { 4308 minYear: 2000, 4309 maxYear: 2015, 4310 minuteStep: 1 4311 } 4312 } 4313 }); 4314}); 4315</script> 4316**/ 4317 4318/*global moment*/ 4319 4320(function ($) { 4321 "use strict"; 4322 4323 var Constructor = function (options) { 4324 this.init('combodate', options, Constructor.defaults); 4325 4326 //by default viewformat equals to format 4327 if(!this.options.viewformat) { 4328 this.options.viewformat = this.options.format; 4329 } 4330 4331 //try parse combodate config defined as json string in data-combodate 4332 options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true); 4333 4334 //overriding combodate config (as by default jQuery extend() is not recursive) 4335 this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, { 4336 format: this.options.format, 4337 template: this.options.template 4338 }); 4339 }; 4340 4341 $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); 4342 4343 $.extend(Constructor.prototype, { 4344 render: function () { 4345 this.$input.combodate(this.options.combodate); 4346 4347 //"clear" link 4348 /* 4349 if(this.options.clear) { 4350 this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ 4351 e.preventDefault(); 4352 e.stopPropagation(); 4353 this.clear(); 4354 }, this)); 4355 4356 this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); 4357 } 4358 */ 4359 }, 4360 4361 value2html: function(value, element) { 4362 var text = value ? value.format(this.options.viewformat) : ''; 4363 $(element).text(text); 4364 }, 4365 4366 html2value: function(html) { 4367 return html ? moment(html, this.options.viewformat) : null; 4368 }, 4369 4370 value2str: function(value) { 4371 return value ? value.format(this.options.format) : ''; 4372 }, 4373 4374 str2value: function(str) { 4375 return str ? moment(str, this.options.format) : null; 4376 }, 4377 4378 value2submit: function(value) { 4379 return this.value2str(value); 4380 }, 4381 4382 value2input: function(value) { 4383 this.$input.combodate('setValue', value); 4384 }, 4385 4386 input2value: function() { 4387 return this.$input.combodate('getValue', null); 4388 }, 4389 4390 activate: function() { 4391 this.$input.siblings('.combodate').find('select').eq(0).focus(); 4392 }, 4393 4394 /* 4395 clear: function() { 4396 this.$input.data('datepicker').date = null; 4397 this.$input.find('.active').removeClass('active'); 4398 }, 4399 */ 4400 4401 autosubmit: function() { 4402 4403 } 4404 4405 }); 4406 4407 Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 4408 /** 4409 @property tpl 4410 @default <input type="text"> 4411 **/ 4412 tpl:'<input type="text">', 4413 /** 4414 @property inputclass 4415 @default null 4416 **/ 4417 inputclass: null, 4418 /** 4419 Format used for sending value to server. Also applied when convert
4419ing date from <code>data-value</code> attribute.<br> 4420 See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format) 4421 4422 @property format 4423 @type string 4424 @default YYYY-MM-DD 4425 **/ 4426 format:'YYYY-MM-DD', 4427 /** 4428 Format used for displaying date. Also applied when converting date from element's text on init. 4429 If not specified equals to `format`. 4430 4431 @property viewformat 4432 @type string 4433 @default null 4434 **/ 4435 viewformat: null, 4436 /** 4437 Template used for displaying dropdowns. 4438 4439 @property template 4440 @type string 4441 @default D / MMM / YYYY 4442 **/ 4443 template: 'D / MMM / YYYY', 4444 /** 4445 Configuration of combodate. 4446 Full list of options: http://vitalets.github.com/combodate/#docs 4447 4448 @property combodate 4449 @type object 4450 @default null 4451 **/ 4452 combodate: null 4453 4454 /* 4455 (not implemented yet) 4456 Text shown as clear date button. 4457 If <code>false</code> clear button will not be rendered. 4458 4459 @property clear 4460 @type boolean|string 4461 @default 'x clear' 4462 */ 4463 //clear: '× clear' 4464 }); 4465 4466 $.fn.editabletypes.combodate = Constructor; 4467 4468}(window.jQuery)); 4469 4470/* 4471Editableform based on jQuery UI 4472*/ 4473(function ($) { 4474 "use strict"; 4475 4476 $.extend($.fn.editableform.Constructor.prototype, { 4477 initButtons: function() { 4478 var $btn = this.$form.find('.editable-buttons'); 4479 $btn.append($.fn.editableform.buttons); 4480 if(this.options.showbuttons === 'bottom') { 4481 $btn.addClass('editable-buttons-bottom'); 4482 } 4483 4484 this.$form.find('.editable-submit').button({ 4485 icons: { primary: "ui-icon-check" }, 4486 text: false 4487 }).removeAttr('title'); 4488 this.$form.find('.editable-cancel').button({ 4489 icons: { primary: "ui-icon-closethick" }, 4490 text: false 4491 }).removeAttr('title'); 4492 } 4493 }); 4494 4495 //error classes 4496 $.fn.editableform.errorGroupClass = null; 4497 $.fn.editableform.errorBlockClass = 'ui-state-error'; 4498 4499}(window.jQuery)); 4500/** 4501* Editable jQuery UI Tooltip 4502* --------------------- 4503* requires jquery ui 1.9.x 4504*/ 4505(function ($) { 4506 "use strict"; 4507 4508 //extend methods 4509 $.extend($.fn.editableContainer.Popup.prototype, { 4510 containerName: 'tooltip', //jQuery method, aplying the widget 4511 //object name in element's .data() 4512 containerDataName: 'ui-tooltip', 4513 innerCss: '.ui-tooltip-content', 4514 4515 //split options on containerOptions and formOptions 4516 splitOptions: function() { 4517 this.containerOptions = {}; 4518 this.formOptions = {}; 4519 4520 //check that jQueryUI build contains tooltip widget 4521 if(!$.ui[this.containerName]) { 4522 $.error('Please use jQueryUI with "tooltip" widget! http://jqueryui.com/download'); 4523 return; 4524 } 4525 //defaults for tooltip 4526 var cDef = $.ui[this.containerName].prototype.options; 4527 for(var k in this.options) { 4528 if(k in cDef) { 4529 this.containerOptions[k] = this.options[k]; 4530 } else { 4531 this.formOptions[k] = this.options[k]; 4532 } 4533 } 4534 }, 4535 4536 initContainer: function(){ 4537 this.handlePlacement(); 4538 $.extend(this.containerOptions, { 4539 items: '*', 4540 content: ' ', 4541 track: false, 4542 open: $.proxy(function() { 4543 //disable events hiding tooltip by default 4544 this.container()._on(this.container().element, { 4545 mouseleave: function(e){ e.stopImmediatePropagation(); }, 4546 focusout: function(e){ e.stopImmediatePropagation(); } 4547 }); 4548 }, this) 4549 }); 4550 4551 this.call(this.containerOptions); 4552 4553 //disable standart triggering tooltip events 4554 this.container()._off(this.container().element, 'mouseover focusin'); 4555 }, 4556 4557 tip: function() { 4558 return this.container() ? this.container()._find(this.container().element) : null; 4559 }, 4560 4561 innerShow: function() { 4562 this.call('open'); 4563 var label = this.options.title || this.$element.data( "ui-tooltip-title") || this.$element.data( "originalTitle"); 4564 this.tip().find(this.innerCss).empty().append($('<label>').text(label)); 4565 }, 4566 4567 innerHide: function() { 4568 this.call('close'); 4569 }, 4570 4571 innerDestroy: function() { 4572 /* tooltip destroys itself on hide */ 4573 }, 4574 4575 setPosition: function() { 4576 this.tip().position( $.extend({ 4577 of: this.$element 4578 }, this.containerOptions.position ) ); 4579 }, 4580 4581 handlePlacement: function() { 4582 var pos; 4583 switch(this.options.placement) { 4584 case 'top': 4585 pos = { 4586 my: "center bottom-5", 4587 at: "center top", 4588 collision: 'flipfit' 4589 }; 4590 break; 4591 case 'right': 4592 pos = { 4593 my: "left+5 center", 4594 at: "right center", 4595 collision: 'flipfit' 4596 }; 4597 break; 4598 case 'bottom': 4599 pos = { 4600 my: "center top+5", 4601 at: "center bottom", 4602 collision: 'flipfit' 4603 }; 4604 break; 4605 case 'left': 4606 pos = { 4607 my: "right-5 center", 4608 at: "left center", 4609 collision: 'flipfit' 4610 }; 4611 break; 4612 } 4613 4614 this.containerOptions.position = pos; 4615 } 4616 4617 }); 4618 4619}(window.jQuery)); 4620 4621/** 4622jQuery UI Datepicker. 4623Description and examples: http://jqueryui.com/datepicker. 4624This input is also accessible as **date** type. Do not use it together with __bootstrap-datepicker__ as both apply <code>$().datepicker()</code> method. 4625For **i18n** you should include js file from here: https://github.com/jquery/jquery-ui/tree/master/ui/i18n. 4626 4627@class dateui 4628@extends abstractinput 4629@final 4630@example 4631<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> 4632<script> 4633$(function(){ 4634 $('#dob').editable({ 4635 format: 'yyyy-mm-dd', 4636 viewformat: 'dd/mm/yyyy', 4637 datepicker: { 4638 firstDay: 1 4639 } 4640 } 4641 }); 4642}); 4643</script> 4644**/ 4645(function ($) { 4646 "use strict"; 4647 4648 var DateUI = function (options) { 4649 this.init('dateui', options, DateUI.defaults); 4650 this.initPicker(options, DateUI.defaults); 4651 }; 4652 4653 $.fn.editableutils.inherit(DateUI, $.fn.editabletypes.abstractinput); 4654 4655 $.extend(DateUI.prototype, { 4656 initPicker: function(options, defaults) { 4657 //by default viewformat equals to format 4658 if(!this.options.viewformat) { 4659 this.options.viewformat = this.options.format; 4660 } 4661 4662 //correct formats: replace yyyy with yy (for compatibility with bootstrap datepicker) 4663 this.options.viewformat = this.options.viewformat.replace('yyyy', 'yy'); 4664 this.options.format = this.options.format.replace('yyyy', 'yy'); 4665 4666 //overriding datepicker config (as by default jQuery extend() is not recursive) 4667 //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only 4668 this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { 4669 dateFormat: this.options.viewformat 4670 }); 4671 }, 4672 4673 render: function () { 4674 this.$input.datepicker(this.options.datepicker); 4675 4676 //"clear" link 4677 if(this.options.clear) { 4678 this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ 4679 e.preventDefault(); 4680 e.stopPropagation(); 4681 this.clear(); 4682 }, this)); 4683 4684 this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); 4685 } 4686 }, 4687 4688 value2html: function(value, element) { 4689 var text = $.datepicker.formatDate(this.options.viewformat, value); 4690 DateUI.superclass.value2html(text, element); 4691 }, 4692 4693 html2value: function(html) { 4694 if(typeof html !== 'string') { 4695 return html; 4696 } 4697 4698 //if string does not match format, UI datepicker throws exception 4699 var d; 4700 try { 4701 d = $.datepicker.parseDate(this.options.viewformat, html); 4702 } catch(e) {} 4703 4704 return d; 4705 }, 4706 4707 value2str: function(value) { 4708 return $.datepicker.formatDate(this.options.format, value); 4709 }, 4710 4711 str2value: function(str) { 4712 if(typeof str !== 'string') { 4713 return str; 4714 } 4715 4716 //if string does not match format, UI datepicker throws exception 4717 var d; 4718 try { 4719 d = $.datepicker.parseDate(this.options.format, str); 4720 } catch(e) {} 4721 4722 return d; 4723 }, 4724 4725 value2submit: function(value) { 4726 return this.value2str(value); 4727 }, 4728 4729 value2input: function(value) { 4730 this.$input.datepicker('setDate', value); 4731 }, 4732 4733 input2value: function() { 4734 return this.$input.datepicker('getDate'); 4735 }, 4736 4737 activate: function() { 4738 }, 4739 4740 clear: function() { 4741 this.$input.datepicker('setDate', null); 4742 }, 4743 4744 autosubmit: function() { 4745 this.$input.on('mouseup', 'table.ui-datepicker-calendar a.ui-state-default', function(e){ 4746 var $form = $(this).closest('form'); 4747 setTimeout(function() { 4748 $form.submit(); 4749 }, 200); 4750 }); 4751 } 4752 4753 }); 4754 4755 DateUI.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 4756 /** 4757 @property tpl 4758 @default <div></div> 4759 **/ 4760 tpl:'<div class="editable-date"></div>', 4761 /** 4762 @property inputclass 4763 @default null 4764 **/ 4765 inputclass: null, 4766 /** 4767 Format used for sending value to server. Also applied when convert
4767ing date from <code>data-value</code> attribute.<br> 4768 Full list of tokens: http://docs.jquery.com/UI/Datepicker/formatDate 4769 4770 @property format 4771 @type string 4772 @default yyyy-mm-dd 4773 **/ 4774 format:'yyyy-mm-dd', 4775 /** 4776 Format used for displaying date. Also applied when converting date from element's text on init. 4777 If not specified equals to <code>format</code> 4778 4779 @property viewformat 4780 @type string 4781 @default null 4782 **/ 4783 viewformat: null, 4784 4785 /** 4786 Configuration of datepicker. 4787 Full list of options: http://api.jqueryui.com/datepicker 4788 4789 @property datepicker 4790 @type object 4791 @default { 4792 firstDay: 0, 4793 changeYear: true, 4794 changeMonth: true 4795 } 4796 **/ 4797 datepicker: { 4798 firstDay: 0, 4799 changeYear: true, 4800 changeMonth: true, 4801 showOtherMonths: true 4802 }, 4803 /** 4804 Text shown as clear date button. 4805 If <code>false</code> clear button will not be rendered. 4806 4807 @property clear 4808 @type boolean|string 4809 @default 'x clear' 4810 **/ 4811 clear: '× clear' 4812 }); 4813 4814 $.fn.editabletypes.dateui = DateUI; 4815 4816}(window.jQuery)); 4817 4818/** 4819jQuery UI datefield input - modification for inline mode. 4820Shows normal <input type="text"> and binds popup datepicker. 4821Automatically shown in inline mode. 4822 4823@class dateuifield 4824@extends dateui 4825 4826@since 1.4.0 4827**/ 4828(function ($) { 4829 "use strict"; 4830 4831 var DateUIField = function (options) { 4832 this.init('dateuifield', options, DateUIField.defaults); 4833 this.initPicker(options, DateUIField.defaults); 4834 }; 4835 4836 $.fn.editableutils.inherit(DateUIField, $.fn.editabletypes.dateui); 4837 4838 $.extend(DateUIField.prototype, { 4839 render: function () { 4840 // this.$input = this.$tpl.find('input'); 4841 this.$input.datepicker(this.options.datepicker); 4842 $.fn.editabletypes.text.prototype.renderClear.call(this); 4843 }, 4844 4845 value2input: function(value) { 4846 this.$input.val($.datepicker.formatDate(this.options.viewformat, value)); 4847 }, 4848 4849 input2value: function() { 4850 return this.html2value(this.$input.val()); 4851 }, 4852 4853 activate: function() { 4854 $.fn.editabletypes.text.prototype.activate.call(this); 4855 }, 4856 4857 toggleClear: function() { 4858 $.fn.editabletypes.text.prototype.toggleClear.call(this); 4859 }, 4860 4861 autosubmit: function() { 4862 //reset autosubmit to empty 4863 } 4864 }); 4865 4866 DateUIField.defaults = $.extend({}, $.fn.editabletypes.dateui.defaults, { 4867 /** 4868 @property tpl 4869 @default <input type="text"> 4870 **/ 4871 tpl: '<input type="text"/>', 4872 /** 4873 @property inputclass 4874 @default null 4875 **/ 4876 inputclass: null, 4877 4878 /* datepicker config */ 4879 datepicker: { 4880 showOn: "button", 4881 buttonImage: "http://jqueryui.com/resources/demos/datepicker/images/calendar.gif", 4882 buttonImageOnly: true, 4883 firstDay: 0, 4884 changeYear: true, 4885 changeMonth: true, 4886 showOtherMonths: true 4887 }, 4888 4889 /* disable clear link */ 4890 clear: false 4891 }); 4892 4893 $.fn.editabletypes.dateuifield = DateUIField; 4894 4895}(window.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.