1/*! X-editable - v1.5.3 2* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery 3* http://github.com/vitalets/x-editable 4* Copyright (c) 2018 Vitaliy Potapov; Licensed MIT */ 5/** 6Form with single input element, two buttons and two states: normal/loading. 7Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown. 8Editableform is linked with one of input types, e.g. 'text', 'select' etc. 9 10@class editableform 11@uses text 12@uses textarea 13**/ 14(function ($) { 15 "use strict"; 16 17 var EditableForm = function (div, options) { 18 this.options = $.extend({}, $.fn.editableform.defaults, options); 19 this.$div = $(div); //div, containing form. Not form tag. Not editable-element. 20 if(!this.options.scope) { 21 this.options.scope = this; 22 } 23 //nothing shown after init 24 }; 25 26 EditableForm.prototype = { 27 constructor: EditableForm, 28 initInput: function() { //called once 29 //take input from options (as it is created in editable-element) 30 this.input = this.options.input; 31 32 //set initial value 33 //todo: may be add check: typeof str === 'string' ? 34 this.value = this.input.str2value(this.options.value); 35 36 //prerender: get input.$input 37 this.input.prerender(); 38 }, 39 initTemplate: function() { 40 this.$form = $($.fn.editableform.template); 41 }, 42 initButtons: function() { 43 var $btn = this.$form.find('.editable-buttons'); 44 $btn.append($.fn.editableform.buttons); 45 if(this.options.showbuttons === 'bottom') { 46 $btn.addClass('editable-buttons-bottom'); 47 } 48 }, 49 /** 50 Renders editableform 51 52 @method render 53 **/ 54 render: function() { 55 //init loader 56 this.$loading = $($.fn.editableform.loading); 57 this.$div.empty().append(this.$loading); 58 59 //init form template and buttons 60 this.initTemplate(); 61 if(this.options.showbuttons) { 62 this.initButtons(); 63 } else { 64 this.$form.find('.editable-buttons').remove(); 65 } 66 67 //show loading state 68 this.showLoading(); 69 70 //flag showing is form now saving value to server. 71 //It is needed to wait when closing form. 72 this.isSaving = false; 73 74 /** 75 Fired when rendering starts 76 @event rendering 77 @param {Object} event event object 78 **/ 79 this.$div.triggerHandler('rendering'); 80 81 //init input 82 this.initInput(); 83 84 //append input to form 85 this.$form.find('div.editable-input').append(this.input.$tpl); 86 87 //append form to container 88 this.$div.append(this.$form); 89 90 //render input 91 $.when(this.input.render()) 92 .then($.proxy(function () { 93 //setup input to submit automatically when no buttons shown 94 if(!this.options.showbuttons) { 95 this.input.autosubmit(); 96 } 97 98 //attach 'cancel' handler 99 this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); 100 101 if(this.input.error) { 102 this.error(this.input.error); 103 this.$form.find('.editable-submit').attr('disabled', true); 104 this.input.$input.attr('disabled', true); 105 //prevent form from submitting 106 this.$form.submit(function(e){ e.preventDefault(); }); 107 } else { 108 this.error(false); 109 this.input.$input.removeAttr('disabled'); 110 this.$form.find('.editable-submit').removeAttr('disabled'); 111 var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value; 112 this.input.value2input(value); 113 //attach submit handler 114 this.$form.submit($.proxy(this.submit, this)); 115 } 116 117 /** 118 Fired when form is rendered 119 @event rendered 120 @param {Object} event event object 121 **/ 122 this.$div.triggerHandler('rendered'); 123
124 this.showForm(); 125 126 //call postrender method to perform actions required visibility of form 127 if(this.input.postrender) { 128 this.input.postrender(); 129 } 130 }, this)); 131 }, 132 cancel: function() { 133 /** 134 Fired when form was cancelled by user 135 @event cancel 136 @param {Object} event event object 137 **/ 138 this.$div.triggerHandler('cancel'); 139 }, 140 showLoading: function() { 141 var w, h; 142 if(this.$form) { 143 //set loading size equal to form 144 w = this.$form.outerWidth(); 145 h = this.$form.outerHeight(); 146 if(w) { 147 this.$loading.width(w); 148 } 149 if(h) { 150 this.$loading.height(h); 151 } 152 this.$form.hide(); 153 } else { 154 //stretch loading to fill container width 155 w = this.$loading.parent().width(); 156 if(w) { 157 this.$loading.width(w); 158 } 159 } 160 this.$loading.show(); 161 }, 162 163 showForm: function(activate) { 164 this.$loading.hide(); 165 this.$form.show(); 166 if(activate !== false) { 167 this.input.activate(); 168 } 169 /** 170 Fired when form is shown 171 @event show 172 @param {Object} event event object 173 **/ 174 this.$div.triggerHandler('show'); 175 }, 176 177 error: function(msg) { 178 var $group = this.$form.find('.control-group'), 179 $block = this.$form.find('.editable-error-block'), 180 lines; 181 182 if(msg === false) { 183 $group.removeClass($.fn.editableform.errorGroupClass); 184 $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); 185 } else { 186 //convert newline to <br> for more pretty error display 187 if(msg) { 188 lines = (''+msg).split('\n'); 189 for (var i = 0; i < lines.length; i++) { 190 lines[i] = $('<div>').text(lines[i]).html(); 191 } 192 msg = lines.join('<br>'); 193 } 194 $group.addClass($.fn.editableform.errorGroupClass); 195 $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); 196 } 197 }, 198 199 submit: function(e) { 200 e.stopPropagation(); 201 e.preventDefault(); 202 203 //get new value from input 204 var newValue = this.input.input2value(); 205 206 //validation: if validate returns string or truthy value - means error 207 //if returns object like {newValue: '...'} => submitted value is reassigned to it 208 var error = this.validate(newValue); 209 if ($.type(error) === 'object' && error.newValue !== undefined) { 210 newValue = error.newValue; 211 this.input.value2input(newValue); 212 if(typeof error.msg === 'string') { 213 this.error(error.msg); 214 this.showForm(); 215 return; 216 } 217 } else if (error) { 218 this.error(error); 219 this.showForm(); 220 return; 221 } 222 223 //if value not changed --> trigger 'nochange' event and return 224 /*jslint eqeq: true*/ 225 if (!this.options.savenochange && this.input.value2str(newValue) === this.input.value2str(this.value)) { 226 /*jslint eqeq: false*/ 227 /** 228 Fired when value not changed but form is submitted. Requires savenochange = false. 229 @event nochange 230 @param {Object} event event object 231 **/ 232 this.$div.triggerHandler('nochange'); 233 return; 234 } 235 236 //convert value for submitting to server 237 var submitValue = this.input.value2submit(newValue); 238 239 this.isSaving = true; 240 241 //sending data to server 242 $.when(this.save(submitValue)) 243 .done($.proxy(function(response) { 244 this.isSaving = false;
245 246 //run success callback 247 var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; 248 249 //if success callback returns false --> keep form open and do not activate input 250 if(res === false) { 251 this.error(false); 252 this.showForm(false); 253 return; 254 } 255 256 //if success callback returns string --> keep form open, show error and activate input 257 if(typeof res === 'string') { 258 this.error(res); 259 this.showForm(); 260 return; 261 } 262 263 //if success callback returns object like {newValue: <something>} --> use that value instead of submitted 264 //it is useful if you want to chnage value in url-function 265 if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { 266 newValue = res.newValue; 267 } 268 269 //clear error message 270 this.error(false); 271 this.value = newValue; 272 /** 273 Fired when form is submitted 274 @event save 275 @param {Object} event event object 276 @param {Object} params additional params 277 @param {mixed} params.newValue raw new value 278 @param {mixed} params.submitValue submitted value as string 279 @param {Object} params.response ajax response 280 281 @example 282 $('#form-div').on('save'), function(e, params){ 283 if(params.newValue === 'username') {...} 284 }); 285 **/ 286 this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); 287 }, this)) 288 .fail($.proxy(function(xhr) { 289 this.isSaving = false; 290 291 var msg; 292 if(typeof this.options.error === 'function') { 293 msg = this.options.error.call(this.options.scope, xhr, newValue); 294 } else { 295 msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; 296 } 297 298 this.error(msg); 299 this.showForm(); 300 }, this)); 301 }, 302 303 save: function(submitValue) { 304 //try parse composite pk defined as json string in data-pk 305 this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); 306 307 var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, 308 /* 309 send on server in following cases: 310 1. url is function 311 2. url is string AND (pk defined OR send option = always) 312 */ 313 send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), 314 params; 315 316 if (send) { //send to server 317 this.showLoading(); 318 319 //standard params 320 params = { 321 name: this.options.name || '', 322 value: submitValue, 323 pk: pk 324 }; 325 326 //additional params 327 if(typeof this.options.params === 'function') { 328 params = this.options.params.call(this.options.scope, params); 329 } else { 330 //try parse json in single quotes (from data-params attribute) 331 this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); 332 $.extend(params, this.options.params); 333 } 334 335 if(typeof this.options.url === 'function') { //user's function 336 return this.options.url.call(this.options.scope, params); 337 } else { 338 //send ajax to server and return deferred object 339 return $.ajax($.extend({ 340 url : this.options.url, 341 data : params, 342 type : 'POST' 343 }, this.options.ajaxOptions)); 344 } 345 } 346 }, 347 348 validate: function (value) { 349 if (value === undefined) { 350 value = this.value; 351 } 352 if (typeof this.options.validate === 'function') { 353 return this.options.validate.call(this.options.scope, value); 354 } 355 }, 356 357 option: function(key, value) { 358 if(key in this.options) { 359 this.options[key] = value; 360 } 361 362 if(key === 'value') { 363 this.setValue(value); 364 } 365 366 //do not pass option to input as it is passed in editable-element 367 }, 368 369 setValue: function(value, convertStr) { 370 if(convertStr) { 371 this.value = this.input.str2value(value); 372 } else { 373 this.value = value; 374 } 375 376 //if form is visible, update input 377 if(this.$form && this.$form.is(':visible')) { 378 this.input.value2input(this.value); 379 } 380 } 381 }; 382 383 /* 384 Initialize editableform. Applied to jQuery object. 385 386 @method $().editableform(options) 387 @params {Object} options 388 @example 389 var $form = $('<div>').editableform({ 390 type: 'text', 391 name: 'username', 392 url: '/post', 393 value: 'vitaliy' 394 }); 395 396 //to display form you should call 'render' method 397 $form.editableform('render'); 398 */ 399 $.fn.editableform = function (option) { 400 var args = arguments; 401 return this.each(function () { 402 var $this = $(this), 403 data = $this.data('editableform'), 404 options = typeof option === 'object' && option; 405 if (!data) { 406 $this.data('editableform', (data = new EditableForm(this, options))); 407 } 408 409 if (typeof option === 'string') { //call method 410 data[option].apply(data, Array.prototype.slice.call(args, 1)); 411 } 412 }); 413 }; 414 415 //keep link to constructor to allow inheritance 416 $.fn.editableform.Constructor = EditableForm; 417 418 //defaults 419 $.fn.editableform.defaults = { 420 /* see also defaults for input */ 421 422 /** 423 Type of input. Can be <code>
423text|textarea|select|date|checklist</code> 424 425 @property type 426 @type string 427 @default 'text' 428 **/ 429 type: 'text', 430 /** 431 Url for submit, e.g. <code>'/post'</code> 432 If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks. 433 434 @property url 435 @type string|function 436 @default null 437 @example 438 url: function(params) { 439 var d = new $.Deferred; 440 if(params.value === 'abc') { 441 return d.reject('error message'); //returning error via deferred object 442 } else { 443 //async saving data in js model 444 someModel.asyncSaveMethod({ 445 ..., 446 success: function(){ 447 d.resolve(); 448 } 449 }); 450 return d.promise(); 451 } 452 } 453 **/ 454 url:null, 455 /** 456 Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value). 457 If defined as <code>function</code> - returned object **overwrites** original ajax data. 458 @example 459 params: function(params) { 460 //originally params contain pk, name and value 461 params.a = 1; 462 return params; 463 } 464 465 @property params 466 @type object|function 467 @default null 468 **/ 469 params:null, 470 /** 471 Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute 472 473 @property name 474 @type string 475 @default null 476 **/ 477 name: null, 478 /** 479 Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>. 480 Can be calculated dynamically via function. 481 482 @property pk 483 @type string|object|function 484 @default null 485 **/ 486 pk: null, 487 /** 488 Initial value. If not defined - will be taken from element's content. 489 For __select__ type should be defined (as it is ID of shown text). 490 491 @property value 492 @type string|object 493 @default null 494 **/ 495 value: null, 496 /** 497 Value that will be displayed in input if original field value is empty (`null|undefined|''`). 498 499 @property defaultValue 500 @type string|object 501 @default null 502 @since 1.4.6 503 **/ 504 defaultValue: null, 505 /** 506 Strategy for sending data on server. Can be `auto|always|never`. 507 When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally. 508 509 @property send 510 @type string 511 @default 'auto' 512 **/ 513 send: 'auto', 514 /** 515 Function for client-side validation. If returns string - means validation not passed and string showed as error. 516 Since 1.5.1 you can modify submitted value by returning object from `validate`: 517 `{newValue: '...'}` or `{newValue: '...', msg: '...'}` 518 519 @property validate 520 @type function 521 @default null 522 @example 523 validate: function(value) { 524 if($.trim(value) == '') { 525 return 'This field is required'; 526 } 527 } 528 **/ 529 validate: null, 530 /** 531 Success callback. Called when value successfully sent on server and **response status = 200**. 532 Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code> 533 or `{success: false, msg: "server error"}` you can check it inside this callback. 534 If it returns **string** - means error occured and string is shown as error message. 535 If it returns **object like** `{newValue: <something>}` - it overwrites value, submitted by user 536 (useful when server changes value). 537 Otherwise newValue simply rendered into element. 538 539 @property success 540 @type function 541 @default null 542 @example 543 success: function(response, newValue) { 544 if(!response.success) return response.msg; 545 } 546 **/ 547 success: null, 548 /** 549 Error callback. Called when request failed (response status != 200). 550 Usefull when you want to parse error response and display a custom message. 551 Must return **string** - the message to be displayed in the error block. 552 553 @property error 554 @type function 555 @default null 556 @since 1.4.4 557 @example 558 error: function(response, newValue) { 559 if(response.status === 500) { 560 return 'Service unavailable. Please try later.'; 561 } else { 562 return response.responseText; 563 } 564 } 565 **/ 566 error: null, 567 /** 568 Additional options for submit ajax request. 569 List of values: http://api.jquery.com/jQuery.ajax 570 571 @property ajaxOptions 572 @type object 573 @default null 574 @since 1.1.1 575 @example 576 ajaxOptions: { 577 type: 'put', 578 dataType: 'json' 579 } 580 **/ 581 ajaxOptions: null, 582 /** 583 Where to show buttons: left(true)|bottom|false 584 Form without buttons is auto-submitted. 585 586 @property showbuttons 587 @type boolean|string 588 @default true 589 @since 1.1.1 590 **/ 591 showbuttons: true, 592 /** 593 Scope for callback methods (success, validate). 594 If <code>null</code> means editableform instance itself.
595 596 @property scope 597 @type DOMElement|object 598 @default null 599 @since 1.2.0 600 @private 601 **/ 602 scope: null, 603 /** 604 Whether to save or cancel value when it was not changed but form was submitted 605 606 @property savenochange 607 @type boolean 608 @default false 609 @since 1.2.0 610 **/ 611 savenochange: false 612 }; 613 614 /* 615 Note: following params could redefined in engine: bootstrap or jqueryui: 616 Classes 'control-group' and 'editable-error-block' must always present! 617 */ 618 $.fn.editableform.template = '<form class="form-inline editableform">'+ 619 '<div class="control-group">' + 620 '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+ 621 '<div class="editable-error-block"></div>' + 622 '</div>' + 623 '</form>'; 624 625 //loading div 626 $.fn.editableform.loading = '<div class="editableform-loading"></div>'; 627 628 //buttons 629 $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+ 630 '<button type="button" class="editable-cancel">cancel</button>'; 631 632 //error class attached to control-group 633 $.fn.editableform.errorGroupClass = null; 634 635 //error class attached to editable-error-block 636 $.fn.editableform.errorBlockClass = 'editable-error'; 637 638 //engine 639 $.fn.editableform.engine = 'jquery'; 640}(window.jQuery)); 641 642/** 643* EditableForm utilites 644*/ 645(function ($) { 646 "use strict"; 647 648 //utils 649 $.fn.editableutils = { 650 /** 651 * classic JS inheritance function 652 */ 653 inherit: function (Child, Parent) { 654 var F = function() { }; 655 F.prototype = Parent.prototype; 656 Child.prototype = new F(); 657 Child.prototype.constructor = Child; 658 Child.superclass = Parent.prototype; 659 }, 660 661 /** 662 * set caret position in input 663 * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area 664 */ 665 setCursorPosition: function(elem, pos) { 666 if (elem.setSelectionRange) { 667 try { elem.setSelectionRange(pos, pos); } catch (e) {} 668 } else if (elem.createTextRange) { 669 var range = elem.createTextRange(); 670 range.collapse(true); 671 range.moveEnd('character', pos); 672 range.moveStart('character', pos); 673 range.select(); 674 } 675 }, 676 677 /** 678 * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes) 679 * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}"> 680 * safe = true --> means no exception will be thrown 681 * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery 682 */ 683 tryParseJson: function(s, safe) { 684 if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) { 685 if (safe) { 686 try { 687 /*jslint evil: true*/ 688 s = (new Function('return ' + s))(); 689 /*jslint evil: false*/ 690 } catch (e) {} finally { 691 return s; 692 } 693 } else { 694 /*jslint evil: true*/ 695 s = (new Function('return ' + s))(); 696 /*jslint evil: false*/ 697 } 698 } 699 return s; 700 }, 701 702 /** 703 * slice object by specified keys 704 */ 705 sliceObj: function(obj, keys, caseSensitive /* default: false */) { 706 var key, keyLower, newObj = {}; 707 708 if (!$.isArray(keys) || !keys.length) { 709 return newObj; 710 } 711 712 for (var i = 0; i < keys.length; i++) { 713 key = keys[i]; 714 if (obj.hasOwnProperty(key)) { 715 newObj[key] = obj[key]; 716 } 717 718 if(caseSensitive === true) { 719 continue; 720 } 721 722 //when getting data-* attributes via $.data() it's converted to lowercase. 723 //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery 724 //workaround is code below. 725 keyLower = key.toLowerCase(); 726 if (obj.hasOwnProperty(keyLower)) { 727 newObj[key] = obj[keyLower]; 728 } 729 } 730 731 return newObj; 732 }, 733 734 /* 735 exclude complex objects from $.data() before pass to config 736 */ 737 getConfigData: function($element) { 738 var data = {}; 739 $.each($element[0].dataset, function(k, v) { 740 if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) { 741 data[k] = v; 742 } 743 }); 744 return data; 745 }, 746 747 /* 748 returns keys of object 749 */ 750 objectKeys: function(o) { 751 if (Object.keys) { 752 return Object.keys(o); 753 } else { 754 if (o !== Object(o)) { 755 throw new TypeError('Object.keys called on a non-object'); 756 } 757 var k=[], p; 758 for (p in o) { 759 if (Object.prototype.hasOwnProperty.call(o,p)) { 760 k.push(p); 761 } 762 } 763 return k; 764 } 765 766 }, 767 768 /** 769 method to escape html. 770 **/ 771 escape: function(str) { 772 return $('<div>').text(str).html(); 773 }, 774 775 /* 776 returns array items from sourceData having value property equal or inArray of 'value' 777 */ 778 itemsByValue: function(value, sourceData, valueProp) { 779 if(!sourceData || value === null) { 780 return []; 781 } 782 783 if (typeof(valueProp) !== "function") { 784 var idKey = valueProp || 'value'; 785 valueProp = function (e) { return e[idKey]; }; 786 } 787 788 var isValArray = $.isArray(value),
789 result = [], 790 that = this; 791 792 $.each(sourceData, function(i, o) { 793 if(o.children) { 794 result = result.concat(that.itemsByValue(value, o.children, valueProp)); 795 } else { 796 /*jslint eqeq: true*/ 797 if(isValArray) { 798 if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) { 799 result.push(o); 800 } 801 } else { 802 var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o; 803 if(value == itemValue) { 804 result.push(o); 805 } 806 } 807 /*jslint eqeq: false*/ 808 } 809 }); 810 811 return result; 812 }, 813 814 /* 815 Returns input by options: type, mode. 816 */ 817 createInput: function(options) { 818 var TypeConstructor, typeOptions, input, 819 type = options.type; 820 821 //`date` is some kind of virtual type that is transformed to one of exact types 822 //depending on mode and core lib 823 if(type === 'date') { 824 //inline 825 if(options.mode === 'inline') { 826 if($.fn.editabletypes.datefield) { 827 type = 'datefield'; 828 } else if($.fn.editabletypes.dateuifield) { 829 type = 'dateuifield'; 830 } 831 //popup 832 } else { 833 if($.fn.editabletypes.date) { 834 type = 'date'; 835 } else if($.fn.editabletypes.dateui) { 836 type = 'dateui'; 837 } 838 } 839 840 //if type still `date` and not exist in types, replace with `combodate` that is base input 841 if(type === 'date' && !$.fn.editabletypes.date) { 842 type = 'combodate'; 843 } 844 } 845 846 //`datetime` should be datetimefield in 'inline' mode 847 if(type === 'datetime' && options.mode === 'inline') { 848 type = 'datetimefield'; 849 } 850 851 //change wysihtml5 to textarea for jquery UI and plain versions 852 if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { 853 type = 'textarea'; 854 } 855 856 //create input of specified type. Input will be used for converting value, not in form 857 if(typeof $.fn.editabletypes[type] === 'function') { 858 TypeConstructor = $.fn.editabletypes[type]; 859 typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults)); 860 input = new TypeConstructor(typeOptions); 861 return input; 862 } else { 863 $.error('Unknown type: '+ type); 864 return false; 865 } 866 }, 867 868 //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr 869 supportsTransitions: function () { 870 var b = document.body || document.documentElement, 871 s = b.style, 872 p = 'transition', 873 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; 874 875 if(typeof s[p] === 'string') { 876 return true; 877 } 878 879 // Tests for vendor specific prop 880 p = p.charAt(0).toUpperCase() + p.substr(1); 881 for(var i=0; i<v.length; i++) { 882 if(typeof s[v[i] + p] === 'string') { 883 return true; 884 } 885 } 886 return false; 887 } 888 889 }; 890}(window.jQuery)); 891 892/** 893Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br> 894This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
895Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br> 896Applied as jQuery method. 897 898@class editableContainer 899@uses editableform 900**/ 901(function ($) { 902 "use strict"; 903 904 var Popup = function (element, options) { 905 this.init(element, options); 906 }; 907 908 var Inline = function (element, options) { 909 this.init(element, options); 910 }; 911 912 //methods 913 Popup.prototype = { 914 containerName: null, //method to call container on element 915 containerDataName: null, //object name in element's .data() 916 innerCss: null, //tbd in child class 917 containerClass: 'editable-container editable-popup', //css class applied to container element 918 defaults: {}, //container itself defaults 919 920 init: function(element, options) { 921 this.$element = $(element); 922 //since 1.4.1 container do not use data-* directly as they already merged into options. 923 this.options = $.extend({}, $.fn.editableContainer.defaults, options); 924 this.splitOptions(); 925 926 //set scope of form callbacks to element 927 this.formOptions.scope = this.$element[0]; 928 929 this.initContainer(); 930 931 //flag to hide container, when saving value will finish 932 this.delayedHide = false;
933 934 //bind 'destroyed' listener to destroy container when element is removed from dom 935 this.$element.on('destroyed', $.proxy(function(){ 936 this.destroy(); 937 }, this)); 938 939 //attach document handler to close containers on click / escape 940 if(!$(document).data('editable-handlers-attached')) { 941 //close all on escape 942 $(document).on('keyup.editable', function (e) { 943 if (e.which === 27) { 944 $('.editable-open').editableContainer('hide', 'cancel'); 945 //todo: return focus on element 946 } 947 }); 948 949 //close containers when click outside 950 //(mousedown could be better than click, it closes everything also on drag drop) 951 $(document).on('click.editable', function(e) { 952 var $target = $(e.target), i, 953 exclude_classes = ['.editable-container', 954 '.ui-datepicker-header', 955 '.datepicker', //in inline mode datepicker is rendered into body 956 '.modal-backdrop', 957 '.bootstrap-wysihtml5-insert-image-modal', 958 '.bootstrap-wysihtml5-insert-link-modal' 959 ]; 960 961 // select2 has extra body click in IE 962 // see: https://github.com/ivaynberg/select2/issues/1058 963 if ($('.select2-drop-mask').is(':visible')) { 964 return; 965 } 966 967 //check if element is detached. It occurs when clicking in bootstrap datepicker 968 if (!$.contains(document.documentElement, e.target)) { 969 return; 970 } 971 972 //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document 973 //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 974 //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec 975 if($target.is(document)) { 976 return; 977 } 978 979 //if click inside one of exclude classes --> no nothing 980 for(i=0; i<exclude_classes.length; i++) { 981 if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) { 982 return; 983 } 984 } 985 986 //close all open containers (except one - target) 987 Popup.prototype.closeOthers(e.target); 988 }); 989 990 $(document).data('editable-handlers-attached', true); 991 } 992 }, 993 994 //split options on containerOptions and formOptions 995 splitOptions: function() { 996 this.containerOptions = {}; 997 this.formOptions = {}; 998 999 if(!$.fn[this.containerName]) { 1000 throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); 1001 } 1002 1003 //keys defined in container defaults go to container, others go to form 1004 for(var k in this.options) { 1005 if(k in this.defaults) { 1006 this.containerOptions[k] = this.options[k]; 1007 } else { 1008 this.formOptions[k] = this.options[k]; 1009 } 1010 } 1011 }, 1012 1013 /* 1014 Returns jquery object of container 1015 @method tip() 1016 */ 1017 tip: function() { 1018 return this.container() ? this.container().$tip : null; 1019 }, 1020 1021 /* returns container object */ 1022 container: function() { 1023 var container; 1024 //first, try get it by `containerDataName` 1025 if(this.containerDataName) { 1026 if(container = this.$element.data(this.containerDataName)) { 1027 return container; 1028 } 1029 }
1030 //second, try `containerName` 1031 container = this.$element.data(this.containerName); 1032 return container; 1033 }, 1034 1035 /* call native method of underlying container, e.g. this.$element.popover('method') */ 1036 call: function() { 1037 this.$element[this.containerName].apply(this.$element, arguments); 1038 }, 1039 1040 initContainer: function(){ 1041 this.call(this.containerOptions); 1042 }, 1043 1044 renderForm: function() { 1045 this.$form 1046 .editableform(this.formOptions) 1047 .on({ 1048 save: $.proxy(this.save, this), //click on submit button (value changed) 1049 nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed) 1050 cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on cancel button 1051 show: $.proxy(function() { 1052 if(this.delayedHide) { 1053 this.hide(this.delayedHide.reason); 1054 this.delayedHide = false;
1055 } else { 1056 this.setPosition(); 1057 } 1058 }, this), //re-position container every time form is shown (occurs each time after loading state) 1059 rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown 1060 resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed 1061 rendered: $.proxy(function(){ 1062 /** 1063 Fired when container is shown and form is rendered (for select will wait for loading dropdown options). 1064 **Note:** Bootstrap popover has own `shown` event that now cannot be separated from x-editable's one. 1065 The workaround is to check `arguments.length` that is always `2` for x-editable. 1066 1067 @event shown 1068 @param {Object} event event object 1069 @example 1070 $('#username').on('shown', function(e, editable) { 1071 editable.input.$input.val('overwriting value of input..'); 1072 }); 1073 **/ 1074 /* 1075 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. 1076 */ 1077 this.$element.triggerHandler('shown', $(this.options.scope).data('editable')); 1078 }, this) 1079 }) 1080 .editableform('render'); 1081 }, 1082 1083 /** 1084 Shows container with form 1085 @method show() 1086 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1087 **/ 1088 /* Note: poshytip owerwrites this method totally! */ 1089 show: function (closeAll) { 1090 this.$element.addClass('editable-open'); 1091 if(closeAll !== false) { 1092 //close all open containers (except this) 1093 this.closeOthers(this.$element[0]); 1094 } 1095 1096 //show container itself 1097 this.innerShow(); 1098 this.tip().addClass(this.containerClass); 1099 1100 /* 1101 Currently, form is re-rendered on every show. 1102 The main reason is that we dont know, what will container do with content when closed: 1103 remove(), detach() or just hide() - it depends on container. 1104 1105 Detaching form itself before hide and re-insert before show is good solution, 1106 but visually it looks ugly --> container changes size before hide. 1107 */ 1108 1109 //if form already exist - delete previous data 1110 if(this.$form) { 1111 //todo: destroy prev data! 1112 //this.$form.destroy(); 1113 } 1114 1115 this.$form = $('<div>'); 1116 1117 //insert form into container body 1118 if(this.tip().is(this.innerCss)) { 1119 //for inline container 1120 this.tip().append(this.$form); 1121 } else { 1122 this.tip().find(this.innerCss).append(this.$form); 1123 } 1124 1125 //render form 1126 this.renderForm(); 1127 }, 1128 1129 /** 1130 Hides container with form 1131 @method hide() 1132 @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code> 1133 **/ 1134 hide: function(reason) { 1135 if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { 1136 return; 1137 } 1138 1139 //if form is saving value, schedule hide 1140 if(this.$form.data('editableform').isSaving) { 1141 this.delayedHide = {reason: reason}; 1142 return; 1143 } else { 1144 this.delayedHide = false;
1145 } 1146 1147 this.$element.removeClass('editable-open'); 1148 this.innerHide(); 1149 1150 /** 1151 Fired when container was hidden. It occurs on both save or cancel. 1152 **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. 1153 The workaround is to check `arguments.length` that is always `2` for x-editable. 1154 1155 @event hidden 1156 @param {object} event event object 1157 @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|manual</code> 1158 @example 1159 $('#username').on('hidden', function(e, reason) { 1160 if(reason === 'save' || reason === 'cancel') { 1161 //auto-open next editable 1162 $(this).closest('tr').next().find('.editable').editable('show'); 1163 } 1164 }); 1165 **/ 1166 this.$element.triggerHandler('hidden', reason || 'manual'); 1167 }, 1168 1169 /* internal show method. To be overwritten in child classes */ 1170 innerShow: function () { 1171 1172 }, 1173 1174 /* internal hide method. To be overwritten in child classes */ 1175 innerHide: function () { 1176 1177 }, 1178 1179 /** 1180 Toggles container visibility (show / hide) 1181 @method toggle() 1182 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1183 **/ 1184 toggle: function(closeAll) { 1185 if(this.container() && this.tip() && this.tip().is(':visible')) { 1186 this.hide(); 1187 } else { 1188 this.show(closeAll); 1189 } 1190 }, 1191 1192 /* 1193 Updates the position of container when content changed. 1194 @method setPosition() 1195 */ 1196 setPosition: function() { 1197 //tbd in child class 1198 }, 1199 1200 save: function(e, params) { 1201 /** 1202 Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance 1203 1204 @event save 1205 @param {Object} event event object 1206 @param {Object} params additional params 1207 @param {mixed} params.newValue submitted value 1208 @param {Object} params.response ajax response 1209 @example 1210 $('#username').on('save', function(e, params) { 1211 //assuming server response: '{success: true}' 1212 var pk = $(this).data('editableContainer').options.pk; 1213 if(params.response && params.response.success) { 1214 alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); 1215 } else { 1216 alert('error!'); 1217 } 1218 }); 1219 **/ 1220 this.$element.triggerHandler('save', params); 1221 1222 //hide must be after trigger, as saving value may require methods of plugin, applied to input 1223 this.hide('save'); 1224 }, 1225 1226 /** 1227 Sets new option 1228 1229 @method option(key, value) 1230 @param {string} key 1231 @param {mixed} value 1232 **/ 1233 option: function(key, value) { 1234 this.options[key] = value; 1235 if(key in this.containerOptions) { 1236 this.containerOptions[key] = value; 1237 this.setContainerOption(key, value); 1238 } else { 1239 this.formOptions[key] = value; 1240 if(this.$form) { 1241 this.$form.editableform('option', key, value); 1242 } 1243 } 1244 }, 1245 1246 setContainerOption: function(key, value) { 1247 this.call('option', key, value); 1248 }, 1249 1250 /** 1251 Destroys the container instance 1252 @method destroy() 1253 **/ 1254 destroy: function() { 1255 this.hide(); 1256 this.innerDestroy(); 1257 this.$element.off('destroyed'); 1258 this.$element.removeData('editableContainer'); 1259 }, 1260
1261 /* to be overwritten in child classes */ 1262 innerDestroy: function() { 1263 1264 }, 1265 1266 /* 1267 Closes other containers except one related to passed element. 1268 Other containers can be cancelled or submitted (depends on onblur option) 1269 */ 1270 closeOthers: function(element) { 1271 $('.editable-open').each(function(i, el){ 1272 //do nothing with passed element and it's children 1273 if(el === element || $(el).find(element).length) { 1274 return; 1275 } 1276 1277 //otherwise cancel or submit all open containers 1278 var $el = $(el), 1279 ec = $el.data('editableContainer'); 1280 1281 if(!ec) { 1282 return; 1283 } 1284 1285 if(ec.options.onblur === 'cancel') { 1286 $el.data('editableContainer').hide('onblur'); 1287 } else if(ec.options.onblur === 'submit') { 1288 $el.data('editableContainer').tip().find('form').submit(); 1289 } 1290 }); 1291 1292 }, 1293 1294 /** 1295 Activates input of visible container (e.g. set focus) 1296 @method activate() 1297 **/ 1298 activate: function() { 1299 if(this.tip && this.tip().is(':visible') && this.$form) { 1300 this.$form.data('editableform').input.activate(); 1301 } 1302 } 1303 1304 }; 1305 1306 /** 1307 jQuery method to initialize editableContainer. 1308 1309 @method $().editableContainer(options) 1310 @params {Object} options 1311 @example 1312 $('#edit').editableContainer({ 1313 type: 'text', 1314 url: '/post', 1315 pk: 1, 1316 value: 'hello' 1317 }); 1318 **/ 1319 $.fn.editableContainer = function (option) { 1320 var args = arguments; 1321 return this.each(function () { 1322 var $this = $(this), 1323 dataKey = 'editableContainer', 1324 data = $this.data(dataKey), 1325 options = typeof option === 'object' && option, 1326 Constructor = (options.mode === 'inline') ? Inline : Popup; 1327 1328 if (!data) { 1329 $this.data(dataKey, (data = new Constructor(this, options))); 1330 } 1331 1332 if (typeof option === 'string') { //call method 1333 data[option].apply(data, Array.prototype.slice.call(args, 1)); 1334 } 1335 }); 1336 }; 1337 1338 //store constructors 1339 $.fn.editableContainer.Popup = Popup; 1340 $.fn.editableContainer.Inline = Inline; 1341 1342 //defaults 1343 $.fn.editableContainer.defaults = { 1344 /** 1345 Initial value of form input 1346 1347 @property value 1348 @type mixed 1349 @default null 1350 @private 1351 **/ 1352 value: null, 1353 /** 1354 Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container. 1355 1356 @property placement 1357 @type string 1358 @default 'top' 1359 **/ 1360 placement: 'top', 1361 /** 1362 Whether to hide container on save/cancel. 1363 1364 @property autohide 1365 @type boolean 1366 @default true 1367 @private 1368 **/ 1369 autohide: true, 1370 /** 1371 Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>. 1372 Setting <code>ignore</code> allows to have several containers open. 1373 1374 @property onblur 1375 @type string 1376 @default 'cancel' 1377 @since 1.1.1 1378 **/ 1379 onblur: 'cancel', 1380 1381 /** 1382 Animation speed (inline mode only) 1383 @property anim 1384 @type string 1385 @default false 1386 **/ 1387 anim: false, 1388 1389 /** 1390 Mode of editable, can be `popup` or `inline` 1391 1392 @property mode 1393 @type string 1394 @default 'popup' 1395 @since 1.4.0 1396 **/ 1397 mode: 'popup' 1398 }; 1399 1400 /* 1401 * workaround to have 'destroyed' event to destroy popover when element is destroyed 1402 * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom 1403 */ 1404 jQuery.event.special.destroyed = { 1405 remove: function(o) { 1406 if (o.handler) { 1407 o.handler(); 1408 } 1409 } 1410 }; 1411 1412}(window.jQuery)); 1413 1414/** 1415* Editable Inline 1416* --------------------- 1417*/ 1418(function ($) { 1419 "use strict"; 1420 1421 //copy prototype from EditableContainer 1422 //extend methods 1423 $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { 1424 containerName: 'editableform', 1425 innerCss: '.editable-inline', 1426 containerClass: 'editable-container editable-inline', //css class applied to container element 1427 1428 initContainer: function(){ 1429 //container is <span> element 1430 this.$tip = $('<span></span>'); 1431 1432 //convert anim to miliseconds (int) 1433 if(!this.options.anim) { 1434 this.options.anim = 0; 1435 } 1436 }, 1437 1438 splitOptions: function() { 1439 //all options are passed to form 1440 this.containerOptions = {}; 1441 this.formOptions = this.options; 1442 }, 1443 1444 tip: function() { 1445 return this.$tip; 1446 }, 1447 1448 innerShow: function () { 1449 this.$element.hide(); 1450 this.tip().insertAfter(this.$element).show(); 1451 }, 1452 1453 innerHide: function () { 1454 this.$tip.hide(this.options.anim, $.proxy(function() { 1455 this.$element.show(); 1456 this.innerDestroy(); 1457 }, this)); 1458 }, 1459 1460 innerDestroy: function() { 1461 if(this.tip()) { 1462 this.tip().empty().remove(); 1463 } 1464 } 1465 }); 1466 1467}(window.jQuery)); 1468/**
1469Makes editable any HTML element on the page. Applied as jQuery method. 1470 1471@class editable 1472@uses editableContainer 1473**/ 1474(function ($) { 1475 "use strict"; 1476 1477 var Editable = function (element, options) { 1478 this.$element = $(element); 1479 //data-* has more priority over js options: because dynamically created elements may change data-* 1480 this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); 1481 if(this.options.selector) { 1482 this.initLive(); 1483 } else { 1484 this.init(); 1485 } 1486 1487 //check for transition support 1488 if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { 1489 this.options.highlight = false; 1490 } 1491 }; 1492 1493 Editable.prototype = { 1494 constructor: Editable, 1495 init: function () { 1496 var isValueByText = false, 1497 doAutotext, finalize; 1498 1499 //name 1500 this.options.name = this.options.name || this.$element.attr('id'); 1501 1502 //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) 1503 //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) 1504 this.options.scope = this.$element[0]; 1505 this.input = $.fn.editableutils.createInput(this.options); 1506 if(!this.input) { 1507 return; 1508 } 1509 1510 //set value from settings or by element's text 1511 if (this.options.value === undefined || this.options.value === null) { 1512 this.value = this.input.html2value($.trim(this.$element.html())); 1513 isValueByText = true; 1514 } else { 1515 /* 1516 value can be string when received from 'data-value' attribute 1517 for complext objects value can be set as json string in data-value attribute, 1518 e.g. data-value="{city: 'Moscow', street: 'Lenina'}" 1519 */ 1520 this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); 1521 if(typeof this.options.value === 'string') { 1522 this.value = this.input.str2value(this.options.value); 1523 } else { 1524 this.value = this.options.value; 1525 } 1526 } 1527 1528 //add 'editable' class to every editable element 1529 this.$element.addClass('editable'); 1530 1531 //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks 1532 if(this.input.type === 'textarea') { 1533 this.$element.addClass('editable-pre-wrapped'); 1534 } 1535 1536 //attach handler activating editable. In disabled mode it just prevent default action (useful for links) 1537 if(this.options.toggle !== 'manual') { 1538 this.$element.addClass('editable-click'); 1539 this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ 1540 //prevent following link if editable enabled 1541 if(!this.options.disabled) { 1542 e.preventDefault(); 1543 } 1544 1545 //stop propagation not required because in document click handler it checks event target 1546 //e.stopPropagation(); 1547 1548 if(this.options.toggle === 'mouseenter') { 1549 //for hover only show container 1550 this.show(); 1551 } else { 1552 //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener 1553 var closeAll = (this.options.toggle !== 'click'); 1554 this.toggle(closeAll); 1555 } 1556 }, this)); 1557 } else { 1558 this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually 1559 } 1560 1561 //if display is function it's far more convinient to have autotext = always to render correctly on init 1562 //see https://github.com/vitalets/x-editable-yii/issues/34 1563 if(typeof this.options.display === 'function') { 1564 this.options.autotext = 'always'; 1565 } 1566 1567 //check conditions for autotext: 1568 switch(this.options.autotext) { 1569 case 'always': 1570 doAutotext = true; 1571 break; 1572 case 'auto': 1573 //if element text is empty and value is defined and value not generated by text --> run autotext 1574 doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; 1575 break; 1576 default: 1577 doAutotext = false;
1578 } 1579 1580 //depending on autotext run render() or just finilize init 1581 $.when(doAutotext ? this.render() : true).then($.proxy(function() { 1582 if(this.options.disabled) { 1583 this.disable(); 1584 } else { 1585 this.enable(); 1586 } 1587 /** 1588 Fired when element was initialized by `$().editable()` method. 1589 Please note that you should setup `init` handler **before** applying `editable`. 1590 1591 @event init 1592 @param {Object} event event object 1593 @param {Object} editable editable instance (as here it cannot accessed via data('editable')) 1594 @since 1.2.0 1595 @example 1596 $('#username').on('init', function(e, editable) { 1597 alert('initialized ' + editable.options.name); 1598 }); 1599 $('#username').editable(); 1600 **/ 1601 this.$element.triggerHandler('init', this); 1602 }, this)); 1603 }, 1604 1605 /* 1606 Initializes parent element for live editables 1607 */ 1608 initLive: function() { 1609 //store selector 1610 var selector = this.options.selector; 1611 //modify options for child elements 1612 this.options.selector = false; 1613 this.options.autotext = 'never'; 1614 //listen toggle events 1615 this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ 1616 var $target = $(e.target).closest(selector); 1617 if(!$target.data('editable')) { 1618 //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) 1619 //see https://github.com/vitalets/x-editable/issues/137 1620 if($target.hasClass(this.options.emptyclass)) { 1621 $target.empty(); 1622 } 1623 $target.editable(this.options).trigger(e); 1624 } 1625 }, this)); 1626 }, 1627 1628 /* 1629 Renders value into element's text. 1630 Can call custom display method from options. 1631 Can return deferred object. 1632 @method render() 1633 @param {mixed} response server response (if exist) to pass into display function 1634 */ 1635 render: function(response) { 1636 //do not display anything 1637 if(this.options.display === false) { 1638 return; 1639 } 1640 1641 //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded 1642 if(this.input.value2htmlFinal) { 1643 return this.input.value2html(this.value, this.$element[0], this.options.display, response); 1644 //if display method defined --> use it 1645 } else if(typeof this.options.display === 'function') { 1646 return this.options.display.call(this.$element[0], this.value, response); 1647 //else use input's original value2html() method 1648 } else { 1649 return this.input.value2html(this.value, this.$element[0]); 1650 } 1651 }, 1652 1653 /** 1654 Enables editable 1655 @method enable() 1656 **/ 1657 enable: function() { 1658 this.options.disabled = false; 1659 this.$element.removeClass('editable-disabled'); 1660 this.handleEmpty(this.isEmpty); 1661 if(this.options.toggle !== 'manual') { 1662 if(this.$element.attr('tabindex') === '-1') { 1663 this.$element.removeAttr('tabindex'); 1664 } 1665 } 1666 }, 1667 1668 /** 1669 Disables editable 1670 @method disable() 1671 **/ 1672 disable: function() { 1673 this.options.disabled = true; 1674 this.hide(); 1675 this.$element.addClass('editable-disabled'); 1676 this.handleEmpty(this.isEmpty);
1677 //do not stop focus on this element 1678 this.$element.attr('tabindex', -1); 1679 }, 1680 1681 /** 1682 Toggles enabled / disabled state of editable element 1683 @method toggleDisabled() 1684 **/ 1685 toggleDisabled: function() { 1686 if(this.options.disabled) { 1687 this.enable(); 1688 } else { 1689 this.disable(); 1690 } 1691 }, 1692 1693 /** 1694 Sets new option 1695 1696 @method option(key, value) 1697 @param {string|object} key option name or object with several options 1698 @param {mixed} value option new value 1699 @example 1700 $('.editable').editable('option', 'pk', 2); 1701 **/ 1702 option: function(key, value) { 1703 //set option(s) by object 1704 if(key && typeof key === 'object') { 1705 $.each(key, $.proxy(function(k, v){ 1706 this.option($.trim(k), v); 1707 }, this)); 1708 return; 1709 } 1710 1711 //set option by string 1712 this.options[key] = value; 1713 1714 //disabled 1715 if(key === 'disabled') { 1716 return value ? this.disable() : this.enable(); 1717 } 1718 1719 //value 1720 if(key === 'value') { 1721 this.setValue(value); 1722 } 1723 1724 //transfer new option to container! 1725 if(this.container) { 1726 this.container.option(key, value); 1727 } 1728 1729 //pass option to input directly (as it points to the same in form) 1730 if(this.input.option) { 1731 this.input.option(key, value); 1732 } 1733 1734 }, 1735 1736 /* 1737 * set emptytext if element is empty 1738 */ 1739 handleEmpty: function (isEmpty) { 1740 //do not handle empty if we do not display anything 1741 if(this.options.display === false) { 1742 return; 1743 } 1744 1745 /* 1746 isEmpty may be set directly as param of method. 1747 It is required when we enable/disable field and can't rely on content 1748 as node content is text: "Empty" that is not empty %) 1749 */ 1750 if(isEmpty !== undefined) { 1751 this.isEmpty = isEmpty; 1752 } else { 1753 //detect empty 1754 //for some inputs we need more smart check 1755 //e.g. wysihtml5 may have <br>, <p></p>, <img> 1756 if(typeof(this.input.isEmpty) === 'function') { 1757 this.isEmpty = this.input.isEmpty(this.$element); 1758 } else { 1759 this.isEmpty = $.trim(this.$element.html()) === ''; 1760 } 1761 } 1762 1763 //emptytext shown only for enabled 1764 if(!this.options.disabled) { 1765 if (this.isEmpty) { 1766 this.$element.html(this.options.emptytext); 1767 if(this.options.emptyclass) { 1768 this.$element.addClass(this.options.emptyclass); 1769 } 1770 } else if(this.options.emptyclass) { 1771 this.$element.removeClass(this.options.emptyclass); 1772 } 1773 } else { 1774 //below required if element disable property was changed 1775 if(this.isEmpty) { 1776 this.$element.empty(); 1777 if(this.options.emptyclass) { 1778 this.$element.removeClass(this.options.emptyclass); 1779 } 1780 } 1781 } 1782 }, 1783 1784 /** 1785 Shows container with form 1786 @method show() 1787 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1788 **/ 1789 show: function (closeAll) { 1790 if(this.options.disabled) { 1791 return; 1792 } 1793 1794 //init editableContainer: popover, tooltip, inline, etc.. 1795 if(!this.container) { 1796 var containerOptions = $.extend({}, this.options, { 1797 value: this.value, 1798 input: this.input //pass input to form (as it is already created) 1799 }); 1800 this.$element.editableContainer(containerOptions); 1801 //listen `save` event 1802 this.$element.on("save.internal", $.proxy(this.save, this)); 1803 this.container = this.$element.data('editableContainer'); 1804 } else if(this.container.tip().is(':visible')) { 1805 return; 1806 } 1807 1808 //show container 1809 this.container.show(closeAll); 1810 }, 1811 1812 /** 1813 Hides container with form 1814 @method hide() 1815 **/ 1816 hide: function () { 1817 if(this.container) { 1818 this.container.hide(); 1819 } 1820 }, 1821 1822 /** 1823 Toggles container visibility (show / hide)
1824 @method toggle() 1825 @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. 1826 **/ 1827 toggle: function(closeAll) { 1828 if(this.container && this.container.tip().is(':visible')) { 1829 this.hide(); 1830 } else { 1831 this.show(closeAll); 1832 } 1833 }, 1834 1835 /* 1836 * called when form was submitted 1837 */ 1838 save: function(e, params) { 1839 //mark element with unsaved class if needed 1840 if(this.options.unsavedclass) { 1841 /* 1842 Add unsaved css to element if: 1843 - url is not user's function 1844 - value was not sent to server 1845 - params.response === undefined, that means data was not sent 1846 - value changed 1847 */ 1848 var sent = false; 1849 sent = sent || typeof this.options.url === 'function'; 1850 sent = sent || this.options.display === false; 1851 sent = sent || params.response !== undefined; 1852 sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); 1853 1854 if(sent) { 1855 this.$element.removeClass(this.options.unsavedclass); 1856 } else { 1857 this.$element.addClass(this.options.unsavedclass); 1858 } 1859 } 1860
1861 //highlight when saving 1862 if(this.options.highlight) { 1863 var $e = this.$element, 1864 bgColor = $e.css('background-color'); 1865 1866 $e.css('background-color', this.options.highlight); 1867 setTimeout(function(){ 1868 if(bgColor === 'transparent') { 1869 bgColor = ''; 1870 } 1871 $e.css('background-color', bgColor); 1872 $e.addClass('editable-bg-transition'); 1873 setTimeout(function(){ 1874 $e.removeClass('editable-bg-transition'); 1875 }, 1700); 1876 }, 10); 1877 } 1878 1879 //set new value 1880 this.setValue(params.newValue, false, params.response); 1881 1882 /** 1883 Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance 1884 1885 @event save 1886 @param {Object} event event object 1887 @param {Object} params additional params 1888 @param {mixed} params.newValue submitted value 1889 @param {Object} params.response ajax response 1890 @example 1891 $('#username').on('save', function(e, params) { 1892 alert('Saved value: ' + params.newValue); 1893 }); 1894 **/ 1895 //event itself is triggered by editableContainer. Description here is only for documentation 1896 }, 1897 1898 validate: function () { 1899 if (typeof this.options.validate === 'function') { 1900 return this.options.validate.call(this, this.value); 1901 } 1902 }, 1903 1904 /** 1905 Sets new value of editable 1906 @method setValue(value, convertStr) 1907 @param {mixed} value new value 1908 @param {boolean} convertStr whether to convert value from string to internal format 1909 **/ 1910 setValue: function(value, convertStr, response) { 1911 if(convertStr) { 1912 this.value = this.input.str2value(value); 1913 } else { 1914 this.value = value; 1915 } 1916 if(this.container) { 1917 this.container.option('value', this.value); 1918 } 1919 $.when(this.render(response)) 1920 .then($.proxy(function() { 1921 this.handleEmpty(); 1922 }, this)); 1923 }, 1924 1925 /** 1926 Activates input of visible container (e.g. set focus) 1927 @method activate() 1928 **/ 1929 activate: function() { 1930 if(this.container) { 1931 this.container.activate(); 1932 } 1933 }, 1934 1935 /** 1936 Removes editable feature from element 1937 @method destroy() 1938 **/ 1939 destroy: function() { 1940 this.disable(); 1941 1942 if(this.container) { 1943 this.container.destroy(); 1944 } 1945 1946 this.input.destroy(); 1947 1948 if(this.options.toggle !== 'manual') { 1949 this.$element.removeClass('editable-click'); 1950 this.$element.off(this.options.toggle + '.editable'); 1951 } 1952 1953 this.$element.off("save.internal"); 1954 1955 this.$element.removeClass('editable editable-open editable-disabled'); 1956 this.$element.removeData('editable'); 1957 } 1958 }; 1959 1960 /* EDITABLE PLUGIN DEFINITION 1961 * ======================= */ 1962 1963 /** 1964 jQuery method to initialize editable element. 1965 1966 @method $().editable(options) 1967 @params {Object} options 1968 @example 1969 $('#username').editable({ 1970 type: 'text', 1971 url: '/post', 1972 pk: 1 1973 }); 1974 **/ 1975 $.fn.editable = function (option) { 1976 //special API methods returning non-jquery object 1977 var result = {}, args = arguments, datakey = 'editable'; 1978 switch (option) { 1979 /** 1980 Runs client-side validation for all matched editables 1981 1982 @method validate() 1983 @returns {Object} validation errors map 1984 @example 1985 $('#username, #fullname').editable('validate'); 1986 // possible result: 1987 { 1988 username: "username is required", 1989 fullname: "fullname should be minimum 3 letters length" 1990 } 1991 **/ 1992 case 'validate': 1993 this.each(function () { 1994 var $this = $(this), data = $this.data(datakey), error; 1995 if (data && (error = data.validate())) { 1996 result[data.options.name] = error; 1997 } 1998 }); 1999 return result; 2000 2001 /** 2002 Returns current values of editable elements. 2003 Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. 2004 If value of some editable is `null` or `undefined` it is excluded from result object. 2005 When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. 2006 2007 @method getValue() 2008 @param {bool} isSingle whether to return just value of single element 2009 @returns {Object} object of element names and values 2010 @example 2011 $('#username, #fullname').editable('getValue'); 2012 //result: 2013 { 2014 username: "superuser", 2015 fullname: "John" 2016 } 2017 //isSingle = true 2018 $('#username').editable('getValue', true); 2019 //result "superuser" 2020 **/ 2021 case 'getValue': 2022 if(arguments.length === 2 && arguments[1] === true) { //isSingle = true 2023 result = this.eq(0).data(datakey).value; 2024 } else { 2025 this.each(function () { 2026 var $this = $(this), data = $this.data(datakey); 2027 if (data && data.value !== undefined && data.value !== null) { 2028 result[data.options.name] = data.input.value2submit(data.value); 2029 } 2030 }); 2031 } 2032 return result; 2033 2034 /** 2035 This method collects values from several editable elements and submit them all to server. 2036 Internally it runs client-side validation for all fields and submits only in case of success. 2037 See <a href="#newrecord">creating new records</a> for details. 2038 Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case 2039 `url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`. 2040 2041 @method submit(options) 2042 @param {object} options 2043 @param {object} options.url url to submit data 2044 @param {object} options.data additional data to submit 2045 @param {object} options.ajaxOptions additional ajax options 2046 @param {function} options.error(obj) error handler 2047 @param {function} options.success(obj,config) success handler 2048 @returns {Object} jQuery object 2049 **/ 2050 case 'submit': //collects value, validate and submit to server for creating new record 2051 var config = arguments[1] || {}, 2052 $elems = this, 2053 errors = this.editable('validate'); 2054 2055 // validation ok 2056 if($.isEmptyObject(errors)) { 2057 var ajaxOptions = {}; 2058 2059 // for single element use url, success etc from options 2060 if($elems.length === 1) { 2061 var editable = $elems.data('editable'); 2062 //standard params 2063 var params = { 2064 name: editable.options.name || '', 2065 value: editable.input.value2submit(editable.value), 2066 pk: (typeof editable.options.pk === 'function') ? 2067 editable.options.pk.call(editable.options.scope) : 2068 editable.options.pk 2069 }; 2070 2071 //additional params 2072 if(typeof editable.options.params === 'function') { 2073 params = editable.options.params.call(editable.options.scope, params); 2074 } else { 2075 //try parse json in single quotes (from data-params attribute) 2076 editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true); 2077 $.extend(params, editable.options.params); 2078 } 2079 2080 ajaxOptions = { 2081 url: editable.options.url, 2082 data: params, 2083 type: 'POST' 2084 }; 2085 2086 // use success / error from options 2087 config.success = config.success || editable.options.success; 2088 config.error = config.error || editable.options.error; 2089 2090 // multiple elements 2091 } else { 2092 var values = this.editable('getValue'); 2093 2094 ajaxOptions = { 2095 url: config.url, 2096 data: values, 2097 type: 'POST' 2098 }; 2099 } 2100 2101 // ajax success callabck (response 200 OK) 2102 ajaxOptions.success = typeof config.success === 'function' ? function(response) { 2103 config.success.call($elems, response, config); 2104 } : $.noop; 2105 2106 // ajax error callabck 2107 ajaxOptions.error = typeof config.error === 'function' ? function() { 2108 config.error.apply($elems, arguments); 2109 } : $.noop; 2110 2111 // extend ajaxOptions 2112 if(config.ajaxOptions) { 2113 $.extend(ajaxOptions, config.ajaxOptions); 2114 } 2115 2116 // extra data 2117 if(config.data) { 2118 $.extend(ajaxOptions.data, config.data); 2119 } 2120 2121 // perform ajax request 2122 $.ajax(ajaxOptions); 2123 } else { //client-side validation error 2124 if(typeof config.error === 'function') { 2125 config.error.call($elems, errors); 2126 } 2127 } 2128 return this; 2129 } 2130 2131 //return jquery object 2132 return this.each(function () { 2133 var $this = $(this), 2134 data = $this.data(datakey), 2135 options = typeof option === 'object' && option; 2136 2137 //for delegated targets do not store `editable` object for element 2138 //it's allows several different selectors. 2139 //see: https://github.com/vitalets/x-editable/issues/312 2140 if(options && options.selector) { 2141 data = new Editable(this, options); 2142 return; 2143 } 2144 2145 if (!data) { 2146 $this.data(datakey, (data = new Editable(this, options))); 2147 } 2148 2149 if (typeof option === 'string') { //call method 2150 data[option].apply(data, Array.prototype.slice.call(args, 1)); 2151 } 2152 }); 2153 }; 2154 2155 2156 $.fn.editable.defaults = { 2157 /** 2158 Type of input. Can be <code>
2158text|textarea|select|date|checklist</code> and more 2159 2160 @property type 2161 @type string 2162 @default 'text' 2163 **/ 2164 type: 'text', 2165 /** 2166 Sets disabled state of editable 2167 2168 @property disabled 2169 @type boolean 2170 @default false 2171 **/ 2172 disabled: false, 2173 /** 2174 How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>. 2175 When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable. 2176 **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element, 2177 you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document. 2178 2179 @example 2180 $('#edit-button').click(function(e) { 2181 e.stopPropagation(); 2182 $('#username').editable('toggle'); 2183 }); 2184 2185 @property toggle 2186 @type string 2187 @default 'click' 2188 **/ 2189 toggle: 'click', 2190 /** 2191 Text shown when element is empty. 2192 2193 @property emptytext 2194 @type string 2195 @default 'Empty' 2196 **/ 2197 emptytext: 'Empty', 2198 /** 2199 Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date. 2200 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>. 2201 <code>auto</code> - text will be automatically set only if element is empty. 2202 <code>always|never</code> - always(never) try to set element's text. 2203 2204 @property autotext 2205 @type string 2206 @default 'auto' 2207 **/ 2208 autotext: 'auto', 2209 /** 2210 Initial value of input. If not set, taken from element's text. 2211 Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option). 2212 For example, to display currency sign: 2213 @example 2214 <a id="price" data-type="text" data-value="100"></a> 2215 <script> 2216 $('#price').editable({ 2217 ... 2218 display: function(value) { 2219 $(this).text(value + '$'); 2220 } 2221 }) 2222 </script> 2223 2224 @property value 2225 @type mixed 2226 @default element's text 2227 **/ 2228 value: null, 2229 /** 2230 Callback to perform custom displaying of value in element's text.
2231 If `null`, default input's display used. 2232 If `false`, no displaying methods will be called, element's text will never change. 2233 Runs under element's scope. 2234 _**Parameters:**_ 2235 2236 * `value` current value to be displayed 2237 * `response` server response (if display called after ajax submit), since 1.4.0 2238 2239 For _inputs with source_ (select, checklist) parameters are different: 2240 2241 * `value` current value to be displayed 2242 * `sourceData` array of items for current input (e.g. dropdown items) 2243 * `response` server response (if display called after ajax submit), since 1.4.0 2244 2245 To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`. 2246 2247 @property display 2248 @type function|boolean 2249 @default null 2250 @since 1.2.0 2251 @example 2252 display: function(value, sourceData) { 2253 //display checklist as comma-separated values 2254 var html = [], 2255 checked = $.fn.editableutils.itemsByValue(value, sourceData); 2256 2257 if(checked.length) { 2258 $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); 2259 $(this).html(html.join(', ')); 2260 } else { 2261 $(this).empty(); 2262 } 2263 } 2264 **/ 2265 display: null, 2266 /** 2267 Css class applied when editable text is empty. 2268 2269 @property emptyclass 2270 @type string 2271 @since 1.4.1 2272 @default editable-empty 2273 **/ 2274 emptyclass: 'editable-empty', 2275 /** 2276 Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). 2277 You may set it to `null` if you work with editables locally and submit them together. 2278 2279 @property unsavedclass 2280 @type string 2281 @since 1.4.1 2282 @default editable-unsaved 2283 **/ 2284 unsavedclass: 'editable-unsaved', 2285 /** 2286 If selector is provided, editable will be delegated to the specified targets. 2287 Usefull for dynamically generated DOM elements. 2288 **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options, 2289 as they actually become editable only after first click. 2290 You should manually set class `editable-click` to these elements. 2291 Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element: 2292 2293 @property selector 2294 @type string 2295 @since 1.4.1 2296 @default null 2297 @example 2298 <div id="user"> 2299 <!-- empty --> 2300 <a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a> 2301 <!-- non-empty --> 2302 <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a> 2303 </div> 2304 2305 <script> 2306 $('#user').editable({ 2307 selector: 'a', 2308 url: '/post', 2309 pk: 1 2310 }); 2311 </script> 2312 **/ 2313 selector: null, 2314 /** 2315 Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. 2316 2317 @property highlight 2318 @type string|boolean 2319 @since 1.4.5 2320 @default #FFFF80 2321 **/ 2322 highlight: '#FFFF80' 2323 }; 2324 2325}(window.jQuery)); 2326 2327/** 2328AbstractInput - base class for all editable inputs. 2329It defines interface to be implemented by any input type. 2330To create your own input you can inherit from this class. 2331 2332@class abstractinput 2333**/ 2334(function ($) { 2335 "use strict"; 2336 2337 //types 2338 $.fn.editabletypes = {}; 2339 2340 var AbstractInput = function () { }; 2341 2342 AbstractInput.prototype = { 2343 /** 2344 Initializes input 2345 2346 @method init() 2347 **/ 2348 init: function(type, options, defaults) { 2349 this.type = type; 2350 this.options = $.extend({}, defaults, options); 2351 }, 2352 2353 /* 2354 this method called before render to init $tpl that is inserted in DOM 2355 */ 2356 prerender: function() { 2357 this.$tpl = $(this.options.tpl); //whole tpl as jquery object 2358 this.$input = this.$tpl; //control itself, can be changed in render method 2359 this.$clear = null; //clear button 2360 this.error = null; //error message, if input cannot be rendered 2361 }, 2362 2363 /** 2364 Renders input from tpl. Can return jQuery deferred object.
2365 Can be overwritten in child objects 2366 2367 @method render() 2368 **/ 2369 render: function() { 2370 2371 }, 2372 2373 /** 2374 Sets element's html by value. 2375 2376 @method value2html(value, element) 2377 @param {mixed} value 2378 @param {DOMElement} element 2379 **/ 2380 value2html: function(value, element) { 2381 $(element)[this.options.escape ? 'text' : 'html']($.trim(value)); 2382 }, 2383 2384 /** 2385 Converts element's html to value 2386 2387 @method html2value(html) 2388 @param {string} html 2389 @returns {mixed} 2390 **/ 2391 html2value: function(html) { 2392 return $('<div>').html(html).text(); 2393 }, 2394 2395 /** 2396 Converts value to string (for internal compare). For submitting to server used value2submit(). 2397 2398 @method value2str(value) 2399 @param {mixed} value 2400 @returns {string} 2401 **/ 2402 value2str: function(value) { 2403 return String(value); 2404 }, 2405 2406 /** 2407 Converts string received from server into value. Usually from `data-value` attribute. 2408 2409 @method str2value(str) 2410 @param {string} str 2411 @returns {mixed} 2412 **/ 2413 str2value: function(str) { 2414 return str; 2415 }, 2416 2417 /** 2418 Converts value for submitting to server. Result can be string or object. 2419 2420 @method value2submit(value) 2421 @param {mixed} value 2422 @returns {mixed} 2423 **/ 2424 value2submit: function(value) { 2425 return value; 2426 }, 2427 2428 /** 2429 Sets value of input. 2430 2431 @method value2input(value) 2432 @param {mixed} value 2433 **/ 2434 value2input: function(value) { 2435 this.$input.val(value); 2436 }, 2437 2438 /** 2439 Returns value of input. Value can be object (e.g. datepicker) 2440 2441 @method input2value() 2442 **/ 2443 input2value: function() { 2444 return this.$input.val(); 2445 }, 2446 2447 /** 2448 Activates input. For text it sets focus. 2449 2450 @method activate() 2451 **/ 2452 activate: function() { 2453 if(this.$input.is(':visible')) { 2454 this.$input.focus(); 2455 } 2456 }, 2457 2458 /** 2459 Creates input. 2460 2461 @method clear() 2462 **/ 2463 clear: function() { 2464 this.$input.val(null); 2465 }, 2466 2467 /** 2468 method to escape html. 2469 **/ 2470 escape: function(str) { 2471 return $('<div>').text(str).html(); 2472 }, 2473 2474 /** 2475 attach handler to automatically submit form when value changed (useful when buttons not shown) 2476 **/ 2477 autosubmit: function() { 2478 2479 }, 2480 2481 /** 2482 Additional actions when destroying element 2483 **/ 2484 destroy: function() { 2485 }, 2486 2487 // -------- helper functions -------- 2488 setClass: function() { 2489 if(this.options.inputclass) { 2490 this.$input.addClass(this.options.inputclass); 2491 } 2492 }, 2493 2494 setAttr: function(attr) { 2495 if (this.options[attr] !== undefined && this.options[attr] !== null) { 2496 this.$input.attr(attr, this.options[attr]); 2497 } 2498 }, 2499 2500 option: function(key, value) { 2501 this.options[key] = value; 2502 } 2503 2504 }; 2505 2506 AbstractInput.defaults = { 2507 /** 2508 HTML template of input. Normally you should not change it. 2509 2510 @property tpl 2511 @type string 2512 @default '' 2513 **/ 2514 tpl: '', 2515 /** 2516 CSS class automatically applied to input 2517 2518 @property inputclass 2519 @type string 2520 @default null 2521 **/ 2522 inputclass: null, 2523 2524 /** 2525 If `true` - html will be escaped in content of element via $.text() method. 2526 If `false` - html will not be escaped, $.html() used. 2527 When you use own `display` function, this option obviosly has no effect. 2528 2529 @property escape 2530 @type boolean 2531 @since 1.5.0 2532 @default true 2533 **/ 2534 escape: true, 2535 2536 //scope for external methods (e.g. source defined as function) 2537 //for internal use only 2538 scope: null, 2539 2540 //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) 2541 showbuttons: true 2542 }; 2543 2544 $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); 2545 2546}(window.jQuery)); 2547 2548/** 2549List - abstract class for inputs that have source option loaded from js array or via ajax 2550 2551@class list 2552@extends abstractinput 2553**/ 2554(function ($) { 2555 "use strict"; 2556 2557 var List = function (options) { 2558 2559 }; 2560 2561 $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); 2562 2563 $.extend(List.prototype, { 2564 render: function () { 2565 var deferred = $.Deferred(); 2566 2567 this.error = null; 2568 this.onSourceReady(function () { 2569 this.renderList(); 2570 deferred.resolve(); 2571 }, function () { 2572 this.error = this.options.sourceError; 2573 deferred.resolve(); 2574 }); 2575 2576 return deferred.promise(); 2577 }, 2578 2579 html2value: function (html) { 2580 return null; //can't set value by text 2581 }, 2582 2583 value2html: function (value, element, display, response) { 2584 var deferred = $.Deferred(), 2585 success = function () { 2586 if(typeof display === 'function') { 2587 //custom display method 2588 display.call(element, value, this.sourceData, response); 2589 } else { 2590 this.value2htmlFinal(value, element); 2591 } 2592 deferred.resolve(); 2593 }; 2594 2595 //for null value just call success without loading source 2596 if(value === null) { 2597 success.call(this); 2598 } else { 2599 this.onSourceReady(success, function () { deferred.resolve(); }); 2600 } 2601 2602 return deferred.promise(); 2603 }, 2604 2605 // ------------- additional functions ------------ 2606 2607 onSourceReady: function (success, error) { 2608 //run source if it function 2609 var source; 2610 if ($.isFunction(this.options.source)) { 2611 source = this.options.source.call(this.options.scope); 2612 this.sourceData = null; 2613 //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed 2614 } else { 2615 source = this.options.source; 2616 } 2617 2618 //if allready loaded just call success 2619 if(this.options.sourceCache && $.isArray(this.sourceData)) { 2620 success.call(this); 2621 return; 2622 } 2623 2624 //try parse json in single quotes (for double quotes jquery does automatically) 2625 try { 2626 source = $.fn.editableutils.tryParseJson(source, false); 2627 } catch (e) { 2628 error.call(this); 2629 return; 2630 } 2631 2632 //loading from url 2633 if (typeof source === 'string') { 2634 //try to get sourceData from cache 2635 if(this.options.sourceCache) { 2636 var cacheID = source, 2637 cache; 2638 2639 if (!$(document).data(cacheID)) { 2640 $(document).data(cacheID, {}); 2641 } 2642 cache = $(document).data(cacheID); 2643 2644 //check for cached data 2645 if (cache.loading === false && cache.sourceData) { //take source from cache 2646 this.sourceData = cache.sourceData; 2647 this.doPrepend(); 2648 success.call(this); 2649 return; 2650 } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later 2651 cache.callbacks.push($.proxy(function () { 2652 this.sourceData = cache.sourceData; 2653 this.doPrepend(); 2654 success.call(this); 2655 }, this)); 2656 2657 //also collecting error callbacks 2658 cache.err_callbacks.push($.proxy(error, this)); 2659 return; 2660 } else { //no cache yet, activate it 2661 cache.loading = true; 2662 cache.callbacks = []; 2663 cache.err_callbacks = []; 2664 } 2665 } 2666
2667 //ajaxOptions for source. Can be overwritten bt options.sourceOptions 2668 var ajaxOptions = $.extend({ 2669 url: source, 2670 type: 'get', 2671 cache: false, 2672 dataType: 'json', 2673 success: $.proxy(function (data) { 2674 if(cache) { 2675 cache.loading = false; 2676 } 2677 this.sourceData = this.makeArray(data); 2678 if($.isArray(this.sourceData)) { 2679 if(cache) { 2680 //store result in cache 2681 cache.sourceData = this.sourceData; 2682 //run success callbacks for other fields waiting for this source 2683 $.each(cache.callbacks, function () { this.call(); }); 2684 } 2685 this.doPrepend(); 2686 success.call(this); 2687 } else { 2688 error.call(this); 2689 if(cache) { 2690 //run error callbacks for other fields waiting for this source 2691 $.each(cache.err_callbacks, function () { this.call(); }); 2692 } 2693 } 2694 }, this), 2695 error: $.proxy(function () { 2696 error.call(this); 2697 if(cache) { 2698 cache.loading = false; 2699 //run error callbacks for other fields 2700 $.each(cache.err_callbacks, function () { this.call(); }); 2701 } 2702 }, this) 2703 }, this.options.sourceOptions); 2704 2705 //loading sourceData from server 2706 $.ajax(ajaxOptions); 2707 2708 } else { //options as json/array 2709 this.sourceData = this.makeArray(source); 2710 2711 if($.isArray(this.sourceData)) { 2712 this.doPrepend(); 2713 success.call(this); 2714 } else { 2715 error.call(this); 2716 } 2717 } 2718 }, 2719 2720 doPrepend: function () { 2721 if(this.options.prepend === null || this.options.prepend === undefined) { 2722 return; 2723 } 2724 2725 if(!$.isArray(this.prependData)) { 2726 //run prepend if it is function (once) 2727 if ($.isFunction(this.options.prepend)) { 2728 this.options.prepend = this.options.prepend.call(this.options.scope); 2729 } 2730 2731 //try parse json in single quotes 2732 this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); 2733 2734 //convert prepend from string to object 2735 if (typeof this.options.prepend === 'string') { 2736 this.options.prepend = {'': this.options.prepend}; 2737 } 2738 2739 this.prependData = this.makeArray(this.options.prepend); 2740 } 2741 2742 if($.isArray(this.prependData) && $.isArray(this.sourceData)) { 2743 this.sourceData = this.prependData.concat(this.sourceData); 2744 } 2745 }, 2746 2747 /* 2748 renders input list 2749 */ 2750 renderList: function() { 2751 // this method should be overwritten in child class 2752 }, 2753 2754 /* 2755 set element's html by value 2756 */ 2757 value2htmlFinal: function(value, element) { 2758 // this method should be overwritten in child class 2759 }, 2760 2761 /** 2762 * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] 2763 */ 2764 makeArray: function(data) { 2765 var count, obj, result = [], item, iterateItem; 2766 if(!data || typeof data === 'string') { 2767 return null; 2768 } 2769 2770 if($.isArray(data)) { //array 2771 /* 2772 function to iterate inside item of array if item is object. 2773 Caclulates count of keys in item and store in obj. 2774 */ 2775 iterateItem = function (k, v) { 2776 obj = {value: k, text: v}; 2777 if(count++ >= 2) { 2778 return false;// exit from `each` if item has more than one key. 2779 } 2780 }; 2781 2782 for(var i = 0; i < data.length; i++) { 2783 item = data[i]; 2784 if(typeof item === 'object') { 2785 count = 0; //count of keys inside item 2786 $.each(item, iterateItem); 2787 //case: [{val1: 'text1'}, {val2: 'text2} ...] 2788 if(count === 1) { 2789 result.push(obj); 2790 //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] 2791 } else if(count > 1) { 2792 //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') 2793 if(item.children) { 2794 item.children = this.makeArray(item.children); 2795 } 2796 result.push(item); 2797 } 2798 } else { 2799 //case: ['text1', 'text2' ...] 2800 result.push({value: item, text: item}); 2801 } 2802 } 2803 } else { //case: {val1: 'text1', val2: 'text2, ...} 2804 $.each(data, function (k, v) { 2805 result.push({value: k, text: v}); 2806 }); 2807 } 2808 return result; 2809 }, 2810 2811 option: function(key, value) { 2812 this.options[key] = value; 2813 if(key === 'source') { 2814 this.sourceData = null; 2815 } 2816 if(key === 'prepend') { 2817 this.prependData = null; 2818 } 2819 } 2820 2821 }); 2822 2823 List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 2824 /** 2825 Source data for list. 2826 If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]` 2827 For compability, object format is also supported: `{"1": "text1", "2"
2827: "text2" ...}` but it does not guarantee elements order. 2828 2829 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. 2830 2831 If **function**, it should return data in format above (since 1.4.0). 2832 2833 Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only): 2834 @example 2835 [ 2836 {text: "group1", children: [ 2837 {value: 1, text: "text1"}, 2838 {value: 2, text: "text2"} 2839 ]}, 2840 ... 2841 ] 2842 2843 2844 @property source 2845 @type string | array | object | function 2846 @default null 2847 **/ 2848 source: null, 2849 /** 2850 Data automatically prepended to the beginning of dropdown list. 2851 2852 @property prepend 2853 @type string | array | object | function 2854 @default false 2855 **/ 2856 prepend: false, 2857 /** 2858 Error message when list cannot be loaded (e.g. ajax error) 2859 2860 @property sourceError 2861 @type string 2862 @default Error when loading list 2863 **/ 2864 sourceError: 'Error when loading list', 2865 /** 2866 if `true` and source is **string url** - results will be cached for fields with the same source. 2867 Usefull for editable column in grid to prevent extra requests. 2868 2869 @property sourceCache 2870 @type boolean 2871 @default true 2872 @since 1.2.0 2873 **/ 2874 sourceCache: true, 2875 /** 2876 Additional ajax options to be used in $.ajax() when loading list from server. 2877 Useful to send extra parameters or change request method. 2878 2879 @property sourceOptions 2880 @type object|function 2881 @default null 2882 @since 1.5.0 2883 @example 2884 sourceOptions: { 2885 data: {param: 123}, 2886 type: 'post' 2887 } 2888 2889 **/ 2890 sourceOptions: null 2891 }); 2892 2893 $.fn.editabletypes.list = List; 2894 2895}(window.jQuery)); 2896 2897/** 2898Text input 2899 2900@class text 2901@extends abstractinput 2902@final 2903@example 2904<a href="#" id="username" data-type="text" data-pk="1">awesome</a> 2905<script> 2906$(function(){ 2907 $('#username').editable({ 2908 url: '/post', 2909 title: 'Enter username' 2910 }); 2911}); 2912</script> 2913**/ 2914(function ($) { 2915 "use strict"; 2916 2917 var Text = function (options) { 2918 this.init('text', options, Text.defaults); 2919 }; 2920 2921 $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); 2922 2923 $.extend(Text.prototype, { 2924 render: function() { 2925 this.renderClear(); 2926 this.setClass(); 2927 this.setAttr('placeholder'); 2928 }, 2929 2930 activate: function() { 2931 if(this.$input.is(':visible')) { 2932 this.$input.focus(); 2933// if (this.$input.is('input,textarea') && !this.$input.is('[type="checkbox"],[type="range"],[type="number"],[type="email"]')) { 2934 if (this.$input.is('input,textarea') && !this.$input.is('[type="checkbox"],[type="range"]')) { 2935 $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); 2936 } 2937 2938 if(this.toggleClear) { 2939 this.toggleClear(); 2940 } 2941 } 2942 }, 2943 2944 //render clear button 2945 renderClear: function() { 2946 if (this.options.clear) { 2947 this.$clear = $('<span class="editable-clear-x"></span>'); 2948 this.$input.after(this.$clear) 2949 .css('padding-right', 24) 2950 .keyup($.proxy(function(e) { 2951 //arrows, enter, tab, etc 2952 if(~$.inArray(e.keyCode, [40,38,9,13,27])) { 2953 return; 2954 } 2955 2956 clearTimeout(this.t); 2957 var that = this; 2958 this.t = setTimeout(function() { 2959 that.toggleClear(e); 2960 }, 100); 2961 2962 }, this)) 2963 .parent().css('position', 'relative'); 2964 2965 this.$clear.click($.proxy(this.clear, this)); 2966 } 2967 }, 2968 2969 postrender: function() { 2970 /* 2971 //now `clear` is positioned via css 2972 if(this.$clear) { 2973 //can position clear button only here, when form is shown and height can be calculated 2974// var h = this.$input.outerHeight(true) || 20, 2975 var h = this.$clear.parent().height(), 2976 delta = (h - this.$clear.height()) / 2; 2977 2978 //this.$clear.css({bottom: delta, right: delta}); 2979 } 2980 */ 2981 }, 2982 2983 //show / hide clear button 2984 toggleClear: function(e) { 2985 if(!this.$clear) { 2986 return; 2987 } 2988 2989 var len = this.$input.val().length, 2990 visible = this.$clear.is(':visible'); 2991 2992 if(len && !visible) { 2993 this.$clear.show(); 2994 } 2995 2996 if(!len && visible) { 2997 this.$clear.hide(); 2998 } 2999 }, 3000 3001 clear: function() { 3002 this.$clear.hide(); 3003 this.$input.val('').focus(); 3004 } 3005 }); 3006 3007 Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3008 /** 3009 @property tpl 3010 @default <input type="text"> 3011 **/ 3012 tpl: '<input type="text">', 3013 /** 3014 Placeholder attribute of input. Shown when input is empty. 3015 3016 @property placeholder 3017 @type string 3018 @default null 3019 **/ 3020 placeholder: null, 3021 3022 /** 3023 Whether to show `clear` button 3024 3025 @property clear 3026 @type boolean 3027 @default true 3028 **/ 3029 clear: true 3030 }); 3031 3032 $.fn.editabletypes.text = Text; 3033 3034}(window.jQuery)); 3035 3036/** 3037Textarea input 3038 3039@class textarea 3040@extends abstractinput 3041@final 3042@example 3043<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a> 3044<script> 3045$(function(){ 3046 $('#comments').editable({ 3047 url: '/post', 3048 title: 'Enter comments', 3049 rows: 10 3050 }); 3051}); 3052</script> 3053**/ 3054(function ($) { 3055 "use strict"; 3056 3057 var Textarea = function (options) { 3058 this.init('textarea', options, Textarea.defaults); 3059 }; 3060 3061 $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); 3062 3063 $.extend(Textarea.prototype, { 3064 render: function () { 3065 this.setClass(); 3066 this.setAttr('placeholder'); 3067 this.setAttr('rows'); 3068 3069 //ctrl + enter
3070 this.$input.keydown(function (e) { 3071 if (e.ctrlKey && e.which === 13) { 3072 $(this).closest('form').submit(); 3073 } 3074 }); 3075 }, 3076 3077 //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant! 3078 /* 3079 value2html: function(value, element) { 3080 var html = '', lines; 3081 if(value) { 3082 lines = value.split("\n"); 3083 for (var i = 0; i < lines.length; i++) { 3084 lines[i] = $('<div>').text(lines[i]).html(); 3085 } 3086 html = lines.join('<br>'); 3087 } 3088 $(element).html(html); 3089 }, 3090 3091 html2value: function(html) { 3092 if(!html) { 3093 return ''; 3094 } 3095 3096 var regex = new RegExp(String.fromCharCode(10), 'g'); 3097 var lines = html.split(/<br\s*\/?>/i); 3098 for (var i = 0; i < lines.length; i++) { 3099 var text = $('<div>').html(lines[i]).text(); 3100 3101 // Remove newline characters (\n) to avoid them being converted by value2html() method 3102 // thus adding extra <br> tags 3103 text = text.replace(regex, ''); 3104 3105 lines[i] = text; 3106 } 3107 return lines.join("\n"); 3108 }, 3109 */ 3110 activate: function() { 3111 $.fn.editabletypes.text.prototype.activate.call(this); 3112 } 3113 }); 3114 3115 Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3116 /** 3117 @property tpl 3118 @default <textarea></textarea> 3119 **/ 3120 tpl:'<textarea></textarea>', 3121 /** 3122 @property inputclass 3123 @default input-large 3124 **/ 3125 inputclass: 'input-large', 3126 /** 3127 Placeholder attribute of input. Shown when input is empty. 3128 3129 @property placeholder 3130 @type string 3131 @default null 3132 **/ 3133 placeholder: null, 3134 /** 3135 Number of rows in textarea 3136 3137 @property rows 3138 @type integer 3139 @default 7 3140 **/ 3141 rows: 7 3142 }); 3143 3144 $.fn.editabletypes.textarea = Textarea; 3145 3146}(window.jQuery)); 3147 3148/** 3149Select (dropdown) 3150 3151@class select 3152@extends list 3153@final 3154@example 3155<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-title="Select status"></a> 3156<script> 3157$(function(){ 3158 $('#status').editable({ 3159 value: 2, 3160 source: [ 3161 {value: 1, text: 'Active'}, 3162 {value: 2, text: 'Blocked'}, 3163 {value: 3, text: 'Deleted'} 3164 ] 3165 }); 3166}); 3167</script> 3168**/ 3169(function ($) { 3170 "use strict"; 3171 3172 var Select = function (options) { 3173 this.init('select', options, Select.defaults); 3174 }; 3175 3176 $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); 3177 3178 $.extend(Select.prototype, { 3179 renderList: function() { 3180 this.$input.empty(); 3181 var escape = this.options.escape; 3182 3183 var fillItems = function($el, data) { 3184 var attr; 3185 if($.isArray(data)) { 3186 for(var i=0; i<data.length; i++) { 3187 attr = {}; 3188 if(data[i].children) { 3189 attr.label = data[i].text; 3190 $el.append(fillItems($('<optgroup>', attr), data[i].children)); 3191 } else { 3192 attr.value = data[i].value; 3193 if(data[i].disabled) { 3194 attr.disabled = true; 3195 } 3196 var $option = $('<option>', attr); 3197 $option[escape ? 'text' : 'html'](data[i].text); 3198 $el.append($option); 3199 } 3200 } 3201 } 3202 return $el; 3203 }; 3204 3205 fillItems(this.$input, this.sourceData); 3206 3207 this.setClass(); 3208 3209 //enter submit 3210 this.$input.on('keydown.editable', function (e) { 3211 if (e.which === 13) { 3212 $(this).closest('form').submit(); 3213 } 3214 }); 3215 }, 3216 3217 value2htmlFinal: function(value, element) { 3218 var text = '', 3219 items = $.fn.editableutils.itemsByValue(value, this.sourceData); 3220 3221 if(items.length) { 3222 text = items[0].text; 3223 } 3224 3225 //$(element).text(text); 3226 $.fn.editabletypes.abstractinput.prototype.value2html.call(this, text, element); 3227 }, 3228 3229 autosubmit: function() { 3230 this.$input.off('keydown.editable').on('change.editable', function(){ 3231 $(this).closest('form').submit(); 3232 }); 3233 } 3234 }); 3235 3236 Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { 3237 /** 3238 @property tpl 3239 @default <select></select> 3240 **/ 3241 tpl:'<select></select>' 3242 }); 3243 3244 $.fn.editabletypes.select = Select; 3245 3246}(window.jQuery)); 3247 3248/** 3249List of checkboxes. 3250Internally value stored as javascript array of values. 3251 3252@class checklist 3253@extends list 3254@final 3255@example 3256<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-title="Select options"></a> 3257<script> 3258$(function(){ 3259 $('#options').editable({ 3260 value: [2, 3], 3261 source: [ 3262 {value: 1, text: 'option1'}, 3263 {value: 2, text: 'option2'}, 3264 {value: 3, text: 'option3'} 3265 ] 3266 }); 3267}); 3268</script> 3269**/ 3270(function ($) { 3271 "use strict"; 3272 3273 var Checklist = function (options) { 3274 this.init('checklist', options, Checklist.defaults); 3275 }; 3276 3277 $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); 3278 3279 $.extend(Checklist.prototype, { 3280 renderList: function() { 3281 var $label, $div; 3282 3283 this.$tpl.empty(); 3284 3285 if(!$.isArray(this.sourceData)) { 3286 return; 3287 } 3288 3289 for(var i=0; i<this.sourceData.length; i++) { 3290 $label = $('<label>').append($('<input>', { 3291 type: 'checkbox', 3292 value: this.sourceData[i].value 3293 })); 3294 var $option = $('<span>'); 3295 $option[this.options.escape ? 'text' : 'html'](' '+this.sourceData[i].text); 3296 $label.append($option); 3297 3298 $('<div>').append($label).appendTo(this.$tpl); 3299 } 3300
3301 this.$input = this.$tpl.find('input[type="checkbox"]'); 3302 this.setClass(); 3303 }, 3304 3305 value2str: function(value) { 3306 return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; 3307 }, 3308 3309 //parse separated string 3310 str2value: function(str) { 3311 var reg, value = null; 3312 if(typeof str === 'string' && str.length) { 3313 reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*'); 3314 value = str.split(reg); 3315 } else if($.isArray(str)) { 3316 value = str; 3317 } else { 3318 value = [str]; 3319 } 3320 return value; 3321 }, 3322 3323 //set checked on required checkboxes 3324 value2input: function(value) { 3325 this.$input.prop('checked', false); 3326 if($.isArray(value) && value.length) { 3327 this.$input.each(function(i, el) { 3328 var $el = $(el); 3329 // cannot use $.inArray as it performs strict comparison 3330 $.each(value, function(j, val){ 3331 /*jslint eqeq: true*/ 3332 if($el.val() == val) { 3333 /*jslint eqeq: false*/ 3334 $el.prop('checked', true); 3335 } 3336 }); 3337 }); 3338 } 3339 }, 3340 3341 input2value: function() { 3342 var checked = []; 3343 this.$input.filter(':checked').each(function(i, el) { 3344 checked.push($(el).val()); 3345 }); 3346 return checked; 3347 }, 3348 3349 //collect text of checked boxes 3350 value2htmlFinal: function(value, element) { 3351 var html = [], 3352 checked = $.fn.editableutils.itemsByValue(value, this.sourceData), 3353 escape = this.options.escape; 3354 3355 if(checked.length) { 3356 $.each(checked, function(i, v) { 3357 var text = escape ? $.fn.editableutils.escape(v.text) : v.text; 3358 html.push(text); 3359 }); 3360 $(element).html(html.join('<br>')); 3361 } else { 3362 $(element).empty(); 3363 } 3364 }, 3365 3366 activate: function() { 3367 this.$input.first().focus(); 3368 }, 3369 3370 autosubmit: function() { 3371 this.$input.on('keydown', function(e){ 3372 if (e.which === 13) { 3373 $(this).closest('form').submit(); 3374 } 3375 }); 3376 } 3377 }); 3378 3379 Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { 3380 /** 3381 @property tpl 3382 @default <div></div> 3383 **/ 3384 tpl:'<div class="editable-checklist"></div>', 3385 3386 /** 3387 @property inputclass 3388 @type string 3389 @default null 3390 **/ 3391 inputclass: null, 3392 3393 /** 3394 Separator of values when reading from `data-value` attribute 3395 3396 @property separator 3397 @type string 3398 @default ',' 3399 **/ 3400 separator: ',' 3401 }); 3402 3403 $.fn.editabletypes.checklist = Checklist; 3404 3405}(window.jQuery)); 3406 3407/** 3408HTML5 input types. 3409Following types are supported: 3410 3411* password 3412* email 3413* url 3414* tel 3415* number 3416* range 3417* time 3418 3419Learn more about html5 inputs: 3420http://www.w3.org/wiki/HTML5_form_additions 3421To check browser compatibility please see: 3422https://developer.mozilla.org/en-US/docs/HTML/Element/Input 3423 3424@class html5types 3425@extends text 3426@final 3427@since 1.3.0 3428@example 3429<a href="#" id="email" data-type="email" data-pk="1">[email protected]</a> 3430<script> 3431$(function(){ 3432 $('#email').editable({ 3433 url: '/post', 3434 title: 'Enter email' 3435 }); 3436}); 3437</script> 3438**/ 3439 3440/** 3441@property tpl 3442@default depends on type 3443**/ 3444 3445/* 3446Password 3447*/ 3448(function ($) { 3449 "use strict"; 3450 3451 var Password = function (options) { 3452 this.init('password', options, Password.defaults); 3453 }; 3454 $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); 3455 $.extend(Password.prototype, { 3456 //do not display password, show '[hidden]' instead 3457 value2html: function(value, element) { 3458 if(value) { 3459 $(element).text('[hidden]'); 3460 } else { 3461 $(element).empty(); 3462 } 3463 }, 3464 //as password not displayed, should not set value by html 3465 html2value: function(html) { 3466 return null; 3467 } 3468 }); 3469 Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3470 tpl: '<input type="password">' 3471 }); 3472 $.fn.editabletypes.password = Password; 3473}(window.jQuery)); 3474 3475 3476/* 3477Email 3478*/ 3479(function ($) { 3480 "use strict"; 3481 3482 var Email = function (options) { 3483 this.init('email', options, Email.defaults); 3484 }; 3485 $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); 3486 Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3487 tpl: '<input type="email">' 3488 }); 3489 $.fn.editabletypes.email = Email; 3490}(window.jQuery)); 3491 3492 3493/* 3494Url 3495*/ 3496(function ($) { 3497 "use strict"; 3498 3499 var Url = function (options) { 3500 this.init('url', options, Url.defaults); 3501 }; 3502 $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); 3503 Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3504 tpl: '<input type="url">' 3505 }); 3506 $.fn.editabletypes.url = Url; 3507}(window.jQuery)); 3508 3509 3510/* 3511Tel 3512*/ 3513(function ($) { 3514 "use strict"; 3515 3516 var Tel = function (options) { 3517 this.init('tel', options, Tel.defaults); 3518 }; 3519 $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); 3520 Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3521 tpl: '<input type="tel">' 3522 }); 3523 $.fn.editabletypes.tel = Tel; 3524}(window.jQuery)); 3525 3526 3527/* 3528Number 3529*/ 3530(function ($) { 3531 "use strict"; 3532 3533 var NumberInput = function (options) { 3534 this.init('number', options, NumberInput.defaults); 3535 }; 3536 $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); 3537 $.extend(NumberInput.prototype, { 3538 render: function () { 3539 NumberInput.superclass.render.call(this); 3540 this.setAttr('min'); 3541 this.setAttr('max'); 3542 this.setAttr('step'); 3543 }, 3544 postrender: function() { 3545 if(this.$clear) {
3546 //increase right ffset for up/down arrows 3547 this.$clear.css({right: 24}); 3548 /* 3549 //can position clear button only here, when form is shown and height can be calculated 3550 var h = this.$input.outerHeight(true) || 20, 3551 delta = (h - this.$clear.height()) / 2; 3552 3553 //add 12px to offset right for up/down arrows 3554 this.$clear.css({top: delta, right: delta + 16}); 3555 */ 3556 } 3557 } 3558 }); 3559 NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { 3560 tpl: '<input type="number">', 3561 inputclass: 'input-mini', 3562 min: null, 3563 max: null, 3564 step: null 3565 }); 3566 $.fn.editabletypes.number = NumberInput; 3567}(window.jQuery)); 3568 3569 3570/* 3571Range (inherit from number) 3572*/ 3573(function ($) { 3574 "use strict"; 3575 3576 var Range = function (options) { 3577 this.init('range', options, Range.defaults); 3578 }; 3579 $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); 3580 $.extend(Range.prototype, { 3581 render: function () { 3582 this.$input = this.$tpl.filter('input'); 3583 3584 this.setClass(); 3585 this.setAttr('min'); 3586 this.setAttr('max'); 3587 this.setAttr('step'); 3588 3589 this.$input.on('input', function(){ 3590 $(this).siblings('output').text($(this).val()); 3591 }); 3592 }, 3593 activate: function() { 3594 this.$input.focus(); 3595 } 3596 }); 3597 Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { 3598 tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>', 3599 inputclass: 'input-medium' 3600 }); 3601 $.fn.editabletypes.range = Range; 3602}(window.jQuery)); 3603 3604/* 3605Time 3606*/ 3607(function ($) { 3608 "use strict"; 3609 3610 var Time = function (options) { 3611 this.init('time', options, Time.defaults); 3612 }; 3613 //inherit from abstract, as inheritance from text gives selection error. 3614 $.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput); 3615 $.extend(Time.prototype, { 3616 render: function() { 3617 this.setClass(); 3618 } 3619 }); 3620 Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3621 tpl: '<input type="time">' 3622 }); 3623 $.fn.editabletypes.time = Time; 3624}(window.jQuery)); 3625 3626/** 3627Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. 3628Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. 3629 3630You should manually download and include select2 distributive: 3631 3632 <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> 3633 <script src="select2/select2.js"></script> 3634 3635To make it **bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): 3636 3637 <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link> 3638 3639**Note:** currently `autotext` feature does not work for select2 with `ajax` remote source. 3640You need initially put both `data-value` and element's text youself: 3641 3642 <a href="#" data-type="select2" data-value="1">Text1</a> 3643 3644 3645@class select2 3646@extends abstractinput 3647@since 1.4.1 3648@final 3649@example 3650<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a> 3651<script> 3652$(function(){ 3653 //local source 3654 $('#country').editable({ 3655 source: [ 3656 {id: 'gb', text: 'Great Britain'}, 3657 {id: 'us', text: 'United States'}, 3658 {id: 'ru', text: 'Russia'} 3659 ], 3660 select2: { 3661 multiple: true 3662 } 3663 }); 3664 //remote source (simple) 3665 $('#country').editable({ 3666 source: '/getCountries', 3667 select2: { 3668 placeholder: 'Select Country', 3669 minimumInputLength: 1 3670 } 3671 }); 3672 //remote source (advanced) 3673 $('#country').editable({ 3674 select2: { 3675 placeholder: 'Select Country', 3676 allowClear: true, 3677 minimumInputLength: 3, 3678 id: function (item) { 3679 return item.CountryId; 3680 }, 3681 ajax: { 3682 url: '/getCountries', 3683 dataType: 'json', 3684 data: function (term, page) { 3685 return { query: term }; 3686 }, 3687 results: function (data, page) { 3688 return { results: data }; 3689 } 3690 }, 3691 formatResult: function (item) { 3692 return item.CountryName; 3693 }, 3694 formatSelection: function (item) { 3695 return item.CountryName; 3696 }, 3697 initSelection: function (element, callback) { 3698 return $.get('/getCountryById', { query: element.val() }, function (data) { 3699 callback(data); 3700 }); 3701 } 3702 } 3703 }); 3704}); 3705</script> 3706**/ 3707(function ($) { 3708 "use strict"; 3709 3710 var Constructor = function (options) { 3711 this.init('select2', options, Constructor.defaults); 3712 3713 options.select2 = options.select2 || {}; 3714 3715 this.sourceData = null; 3716 3717 //placeholder 3718 if(options.placeholder) { 3719 options.select2.placeholder = options.placeholder; 3720 } 3721 3722 //if not `tags` mode, use source 3723 if(!options.select2.tags && options.source) { 3724 var source = options.source; 3725 //if source is function, call it (once!) 3726 if ($.isFunction(options.source)) { 3727 source = options.source.call(options.scope); 3728 } 3729 3730 if (typeof source === 'string') { 3731 options.select2.ajax = options.select2.ajax || {}; 3732 //some default ajax params 3733 if(!options.select2.ajax.data) { 3734 options.select2.ajax.data = function(term) {return { query:term };}; 3735 } 3736 if(!options.select2.ajax.results) { 3737 options.select2.ajax.results = function(data) { return {results:data };}; 3738 } 3739 options.select2.ajax.url = source; 3740 } else { 3741 //check format and convert x-editable format to select2 format (if needed) 3742 this.sourceData = this.convertSource(source); 3743 options.select2.data = this.sourceData; 3744 } 3745 } 3746 3747 //overriding objects in config (as by default jQuery extend() is not recursive) 3748 this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); 3749 3750 //detect whether it is multi-valued 3751 this.isMultiple = this.options.select2.tags || this.options.select2.multiple; 3752 this.isRemote = ('ajax' in this.options.select2); 3753 3754 //store function returning ID of item 3755 //should be here as used inautotext for local source 3756 this.idFunc = this.options.select2.id; 3757 if (typeof(this.idFunc) !== "function") { 3758 var idKey = this.idFunc || 'id'; 3759 this.idFunc = function (e) { return e[idKey]; }; 3760 } 3761 3762 //store function that renders text in select2 3763 this.formatSelection = this.options.select2.formatSelection; 3764 if (typeof(this.formatSelection) !== "function") { 3765 this.formatSelection = function (e) { return e.text; }; 3766 } 3767 }; 3768 3769 $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); 3770 3771 $.extend(Constructor.prototype, { 3772 render: function() { 3773 this.setClass(); 3774 3775 //can not apply select2 here as it calls initSelection 3776 //over input that does not have correct value yet. 3777 //apply select2 only in value2input 3778 //this.$input.select2(this.options.select2); 3779 3780 //when data is loaded via ajax, we need to know when it's done to populate listData 3781 if(this.isRemote) { 3782 //listen to loaded event to populate data 3783 this.$input.on('select2-loaded', $.proxy(function(e) { 3784 this.sourceData = e.items.results; 3785 }, this)); 3786 } 3787 3788 //trigger resize of editableform to re-position container in multi-valued mode 3789 if(this.isMultiple) { 3790 this.$input.on('change', function() { 3791 $(this).closest('form').parent().triggerHandler('resize'); 3792 }); 3793 } 3794 }, 3795 3796 value2html: function(value, element) { 3797 var text = '', data, 3798 that = this; 3799 3800 if(this.options.select2.tags) { //in tags mode just assign value 3801 data = value; 3802 //data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc); 3803 } else if(this.sourceData) { 3804 data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); 3805 } else { 3806 //can not get list of possible values 3807 //(e.g. autotext for select2 with ajax source) 3808 } 3809 3810 //data may be array (when multiple values allowed) 3811 if($.isArray(data)) { 3812 //collect selected data and show with separator 3813 text = []; 3814 $.each(data, function(k, v){ 3815 text.push(v && typeof v === 'object' ? that.formatSelection(v) : v); 3816 }); 3817 } else if(data) { 3818 text = that.formatSelection(data); 3819 } 3820 3821 text = $.isArray(text) ? text.join(this.options.viewseparator) : text; 3822 3823 //$(element).text(text); 3824 Constructor.superclass.value2html.call(this, text, element); 3825 }, 3826 3827 html2value: function(html) { 3828 return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; 3829 }, 3830 3831 value2input: function(value) { 3832 // if value array => join it anyway 3833 if($.isArray(value)) { 3834 value = value.join(this.getSeparator()); 3835 } 3836 3837 //for remote source just set value, text is updated by initSelection 3838 if(!this.$input.data('select2')) { 3839 this.$input.val(value); 3840 this.$input.select2(this.options.select2); 3841 } else { 3842 //second argument needed to separate initial change from user's click (for autosubmit) 3843 this.$input.val(value).trigger('change', true); 3844 3845 //Uncaught Error: cannot call val() if initSelection() is not defined 3846 //this.$input.select2('val', value); 3847 } 3848 3849 // if defined remote source AND no multiple mode AND no user's initSelection provided --> 3850 // we should somehow get text for provided id. 3851 // The solution is to use element's text as text for that id (exclude empty) 3852 if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) { 3853 // customId and customText are methods to extract `id` and `text` from data object 3854 // we can use this workaround only if user did not define these methods 3855 // otherwise we cant construct data object 3856 var customId = this.options.select2.id, 3857 customText = this.options.select2.formatSelection; 3858 3859 if(!customId && !customText) { 3860 var $el = $(this.options.scope); 3861 if (!$el.data('editable').isEmpty) { 3862 var data = {id: value, text: $el.text()}; 3863 this.$input.select2('data', data); 3864 } 3865 } 3866 } 3867 }, 3868 3869 input2value: function() { 3870 return this.$input.select2('val'); 3871 }, 3872 3873 str2value: function(str, separator) { 3874 if(typeof str !== 'string' || !this.isMultiple) { 3875 return str; 3876 } 3877
3878 separator = separator || this.getSeparator(); 3879 3880 var val, i, l; 3881 3882 if (str === null || str.length < 1) { 3883 return null; 3884 } 3885 val = str.split(separator); 3886 for (i = 0, l = val.length; i < l; i = i + 1) { 3887 val[i] = $.trim(val[i]); 3888 } 3889 3890 return val; 3891 }, 3892 3893 autosubmit: function() { 3894 this.$input.on('change', function(e, isInitial){ 3895 if(!isInitial) { 3896 $(this).closest('form').submit(); 3897 } 3898 }); 3899 }, 3900 3901 getSeparator: function() { 3902 return this.options.select2.separator || $.fn.select2.defaults.separator; 3903 }, 3904 3905 /* 3906 Converts source from x-editable format: {value: 1, text: "1"} to 3907 select2 format: {id: 1, text: "1"} 3908 */ 3909 convertSource: function(source) { 3910 if($.isArray(source) && source.length && source[0].value !== undefined) { 3911 for(var i = 0; i<source.length; i++) { 3912 if(source[i].value !== undefined) { 3913 source[i].id = source[i].value; 3914 delete source[i].value; 3915 } 3916 } 3917 } 3918 return source; 3919 }, 3920 3921 activate: function() { 3922 this.$input.select2('open'); 3923 }, 3924 3925 destroy: function() { 3926 if(this.$input) { 3927 if(this.$input.data('select2')) { 3928 this.$input.select2('destroy'); 3929 } 3930 } 3931 } 3932 3933 }); 3934 3935 Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 3936 /** 3937 @property tpl 3938 @default <input type="hidden"> 3939 **/ 3940 tpl:'<input type="hidden">', 3941 /** 3942 Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). 3943 3944 @property select2 3945 @type object 3946 @default null 3947 **/ 3948 select2: null, 3949 /** 3950 Placeholder attribute of select 3951 3952 @property placeholder 3953 @type string 3954 @default null 3955 **/ 3956 placeholder: null, 3957 /** 3958 Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. 3959 Please note, that format is different from simple `select` input: use 'id' instead of 'value'. 3960 E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. 3961 3962 @property source 3963 @type array|string|function 3964 @default null 3965 **/ 3966 source: null, 3967 /** 3968 Separator used to display tags. 3969 3970 @property viewseparator 3971 @type string 3972 @default ', ' 3973 **/ 3974 viewseparator: ', ' 3975 }); 3976 3977 $.fn.editabletypes.select2 = Constructor; 3978 3979}(window.jQuery)); 3980 3981/** 3982* Combodate - 1.1.0 3983* Dropdown date and time picker. 3984* Converts text input into dropdowns to pick day, month, year, hour, minute and second. 3985* Uses momentjs as datetime library http://momentjs.com. 3986* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang 3987* 3988* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight 3989* In combodate: 3990* 12:00 pm --> 12:00 (24-h format, midday) 3991* 12:00 am --> 00:00 (24-h format, midnight, start of day) 3992* 3993* Differs from momentjs parse rules: 3994* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) 3995* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) 3996* 3997* 3998* Author: Vitaliy Potapov 3999* Project page: http://github.com/vitalets/combodate 4000* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. 4001**/ 4002(function ($) { 4003 4004 var Combodate = function (element, options) { 4005 this.$element = $(element); 4006 if(!this.$element.is('input')) { 4007 $.error('Combodate should be applied to INPUT element'); 4008 return; 4009 } 4010 this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data()); 4011 this.init(); 4012 }; 4013 4014 Combodate.prototype = { 4015 constructor: Combodate, 4016 init: function () { 4017 this.map = { 4018 //key regexp moment.method 4019 day: ['D', 'date'], 4020 month: ['M', 'month'], 4021 year: ['Y', 'year'], 4022 hour: ['[Hh]', 'hours'], 4023 minute: ['m', 'minutes'], 4024 second: ['s', 'seconds'], 4025 ampm: ['[Aa]', ''] 4026 }; 4027 4028 this.$widget = $('<span class="combodate"></span>').html(this.getTemplate()); 4029 4030 this.initCombos(); 4031 4032 // internal momentjs instance 4033 this.datetime = null; 4034 4035 //update original input on change 4036 this.$widget.on('change', 'select', $.proxy(function(e) { 4037 this.$element.val(this.getValue()).change(); 4038 // update days count if month or year changes 4039 if (this.options.smartDays) { 4040 if ($(e.target).is('.month') || $(e.target).is('.year')) { 4041 this.fillCombo('day'); 4042 } 4043 } 4044 }, this)); 4045 4046 this.$widget.find('select').css('width', 'auto'); 4047 4048 // hide original input and insert widget 4049 this.$element.hide().after(this.$widget); 4050 4051 // set initial value 4052 this.setValue(this.$element.val() || this.options.value); 4053 }, 4054 4055 /* 4056 Replace tokens in template with <select> elements 4057 */ 4058 getTemplate: function() { 4059 var tpl = this.options.template; 4060 var inputDisabled = this.$element.prop('disabled'); 4061 var customClass = this.options.customClass; 4062 4063 //first pass 4064 $.each(this.map, function(k, v) { 4065 v = v[0]; 4066 var r = new RegExp(v+'+'), 4067 token = v.length > 1 ? v.substring(1, 2) : v; 4068 4069 tpl = tpl.replace(r, '{'+token+'}'); 4070 }); 4071 4072 //replace spaces with 4073 tpl = tpl.replace(/ /g, ' '); 4074 4075 //second pass 4076 $.each(this.map, function(k, v) { 4077 v = v[0]; 4078 var token = v.length > 1 ? v.substring(1, 2) : v; 4079 4080 tpl = tpl.replace('{'+token+'}', '<select class="'+k+' '+customClass +'"'+ 4081 (inputDisabled ? ' disabled="disabled"' : '')+'></select>'); 4082 }); 4083 4084 return tpl; 4085 }, 4086 4087 /* 4088 Initialize combos that presents in template 4089 */ 4090 initCombos: function() { 4091 for (var k in this.map) { 4092 var $c = this.$widget.find('.'+k); 4093 // set properties like this.$day, this.$month etc. 4094 this['$'+k] = $c.length ? $c : null; 4095 // fill with items 4096 this.fillCombo(k); 4097 } 4098 }, 4099 4100 /* 4101 Fill combo with items 4102 */ 4103 fillCombo: function(k) { 4104 var $combo = this['$'+k]; 4105 if (!$combo) { 4106 return; 4107 } 4108
4109 // define method name to fill items, e.g `fillDays` 4110 var f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); 4111 var items = this[f](); 4112 var value = $combo.val(); 4113 4114 $combo.empty(); 4115 for(var i=0; i<items.length; i++) { 4116 $combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); 4117 } 4118 4119 $combo.val(value); 4120 }, 4121 4122 /* 4123 Initialize items of combos. Handles `firstItem` option 4124 */ 4125 fillCommon: function(key) { 4126 var values = [], 4127 relTime; 4128 4129 if(this.options.firstItem === 'name') { 4130 //need both to support moment ver < 2 and >= 2 4131 if (moment.localeData) { 4132 relTime = moment.localeData()._relativeTime; 4133 } else { 4134 relTime = moment.relativeTime || moment.langData()._relativeTime; 4135 } 4136 var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; 4137 //take last entry (see momentjs lang files structure) 4138 header = header.split(' ').reverse()[0]; 4139 values.push(['', header]); 4140 } else if(this.options.firstItem === 'empty') { 4141 values.push(['', '']); 4142 } 4143 return values; 4144 }, 4145 4146 4147 /* 4148 fill day 4149 */ 4150 fillDay: function() { 4151 var items = this.fillCommon('d'), name, i, 4152 twoDigit = this.options.template.indexOf('DD') !== -1, 4153 daysCount = 31; 4154 4155 // detect days count (depends on month and year) 4156 // originally https://github.com/vitalets/combodate/pull/7 4157 if (this.options.smartDays && this.$month && this.$year) { 4158 var month = parseInt(this.$month.val(), 10); 4159 var year = parseInt(this.$year.val(), 10); 4160 4161 if (!isNaN(month) && !isNaN(year)) { 4162 daysCount = moment([year, month]).daysInMonth(); 4163 } 4164 } 4165 4166 for (i = 1; i <= daysCount; i++) { 4167 name = twoDigit ? this.leadZero(i) : i; 4168 items.push([i, name]); 4169 } 4170 return items; 4171 }, 4172 4173 /* 4174 fill month 4175 */ 4176 fillMonth: function() { 4177 var items = this.fillCommon('M'), name, i, 4178 longNamesNum = this.options.template.indexOf('MMMMMM') !== -1, 4179 shortNamesNum = this.options.template.indexOf('MMMMM') !== -1, 4180 longNames = this.options.template.indexOf('MMMM') !== -1, 4181 shortNames = this.options.template.indexOf('MMM') !== -1, 4182 twoDigit = this.options.template.indexOf('MM') !== -1; 4183 4184 for(i=0; i<=11; i++) { 4185 if (longNamesNum) { 4186 name = moment().date(1).month(i).format('MM - MMMM'); 4187 } else if (shortNamesNum) { 4188 name = moment().date(1).month(i).format('MM - MMM'); 4189 } else if(longNames) { 4190 //see https://github.com/timrwood/momentjs.com/pull/36 4191 name = moment().date(1).month(i).format('MMMM'); 4192 } else if(shortNames) { 4193 name = moment().date(1).month(i).format('MMM'); 4194 } else if(twoDigit) { 4195 name = this.leadZero(i+1); 4196 } else { 4197 name = i+1; 4198 } 4199 items.push([i, name]); 4200 } 4201 return items; 4202 }, 4203 4204 /* 4205 fill year 4206 */ 4207 fillYear: function() { 4208 var items = [], name, i, 4209 longNames = this.options.template.indexOf('YYYY') !== -1; 4210 4211 for(i=this.options.maxYear; i>=this.options.minYear; i--) { 4212 name = longNames ? i : (i+'').substring(2); 4213 items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); 4214 } 4215 4216 items = this.fillCommon('y').concat(items); 4217 4218 return items; 4219 }, 4220 4221 /* 4222 fill hour 4223 */ 4224 fillHour: function() { 4225 var items = this.fillCommon('h'), name, i, 4226 h12 = this.options.template.indexOf('h') !== -1, 4227 h24 = this.options.template.indexOf('H') !== -1, 4228 twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, 4229 min = h12 ? 1 : 0, 4230 max = h12 ? 12 : 23; 4231 4232 for(i=min; i<=max; i++) { 4233 name = twoDigit ? this.leadZero(i) : i; 4234 items.push([i, name]); 4235 } 4236 return items; 4237 }, 4238 4239 /* 4240 fill minute 4241 */ 4242 fillMinute: function() { 4243 var items = this.fillCommon('m'), name, i, 4244 twoDigit = this.options.template.indexOf('mm') !== -1; 4245 4246 for(i=0; i<=59; i+= this.options.minuteStep) { 4247 name = twoDigit ? this.leadZero(i) : i; 4248 items.push([i, name]); 4249 } 4250 return items; 4251 }, 4252 4253 /* 4254 fill second 4255 */ 4256 fillSecond: function() { 4257 var items = this.fillCommon('s'), name, i, 4258 twoDigit = this.options.template.indexOf('ss') !== -1; 4259 4260 for(i=0; i<=59; i+= this.options.secondStep) { 4261 name = twoDigit ? this.leadZero(i) : i; 4262 items.push([i, name]); 4263 } 4264 return items; 4265 }, 4266 4267 /* 4268 fill ampm 4269 */ 4270 fillAmpm: function() { 4271 var ampmL = this.options.template.indexOf('a') !== -1, 4272 ampmU = this.options.template.indexOf('A') !== -1, 4273 items = [ 4274 ['am', ampmL ? 'am' : 'AM'], 4275 ['pm', ampmL ? 'pm' : 'PM'] 4276 ]; 4277 return items; 4278 }, 4279 4280 /* 4281 Returns current date value from combos. 4282 If format not specified - `options.format` used. 4283 If format = `null` - Moment object returned. 4284 */ 4285 getValue: function(format) { 4286 var dt, values = {}, 4287 that = this, 4288 notSelected = false;
4289 4290 //getting selected values 4291 $.each(this.map, function(k, v) { 4292 if(k === 'ampm') { 4293 return; 4294 } 4295 4296 // if combo exists, use it's value, otherwise use default 4297 if (that['$'+k]) { 4298 values[k] = parseInt(that['$'+k].val(), 10); 4299 } else { 4300 var defaultValue; 4301 if (that.datetime) { 4302 defaultValue = that.datetime[v[1]](); 4303 } else { 4304 defaultValue = k === 'day' ? 1 : 0; 4305 } 4306 values[k] = defaultValue; 4307 } 4308 4309 if(isNaN(values[k])) { 4310 notSelected = true; 4311 return false; 4312 } 4313 }); 4314 4315 //if at least one visible combo not selected - return empty string 4316 if(notSelected) { 4317 return ''; 4318 } 4319 4320 //convert hours 12h --> 24h 4321 if(this.$ampm) { 4322 //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) 4323 if(values.hour === 12) { 4324 values.hour = this.$ampm.val() === 'am' ? 0 : 12; 4325 } else { 4326 values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; 4327 } 4328 } 4329 4330 dt = moment([ 4331 values.year, 4332 values.month, 4333 values.day, 4334 values.hour, 4335 values.minute, 4336 values.second 4337 ]); 4338 4339 //highlight invalid date 4340 this.highlight(dt); 4341 4342 format = format === undefined ? this.options.format : format; 4343 if(format === null) { 4344 return dt.isValid() ? dt : null; 4345 } else { 4346 return dt.isValid() ? dt.format(format) : ''; 4347 } 4348 }, 4349 4350 setValue: function(value) { 4351 if(!value) { 4352 return; 4353 } 4354 4355 // parse in strict mode (third param `true`) 4356 var dt = typeof value === 'string' ? moment(value, this.options.format, true) : moment(value), 4357 that = this, 4358 values = {}; 4359 4360 //function to find nearest value in select options 4361 function getNearest($select, value) { 4362 var delta = {}; 4363 $select.children('option').each(function(i, opt){ 4364 var optValue = $(opt).attr('value'), 4365 distance; 4366 4367 if(optValue === '') return; 4368 distance = Math.abs(optValue - value); 4369 if(typeof delta.distance === 'undefined' || distance < delta.distance) { 4370 delta = {value: optValue, distance: distance}; 4371 } 4372 }); 4373 return delta.value; 4374 } 4375 4376 if(dt.isValid()) { 4377 //read values from date object 4378 $.each(this.map, function(k, v) { 4379 if(k === 'ampm') { 4380 return; 4381 } 4382 values[k] = dt[v[1]](); 4383 }); 4384 4385 if(this.$ampm) { 4386 //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) 4387 if(values.hour >= 12) { 4388 values.ampm = 'pm'; 4389 if(values.hour > 12) { 4390 values.hour -= 12; 4391 } 4392 } else { 4393 values.ampm = 'am'; 4394 if(values.hour === 0) { 4395 values.hour = 12; 4396 } 4397 } 4398 } 4399 4400 $.each(values, function(k, v) { 4401 //call val() for each existing combo, e.g. this.$hour.val() 4402 if(that['$'+k]) { 4403 4404 if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { 4405 v = getNearest(that['$'+k], v); 4406 } 4407 4408 if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { 4409 v = getNearest(that['$'+k], v); 4410 } 4411 4412 that['$'+k].val(v); 4413 } 4414 }); 4415 4416 // update days count 4417 if (this.options.smartDays) { 4418 this.fillCombo('day'); 4419 } 4420 4421 this.$element.val(dt.format(this.options.format)).change(); 4422 this.datetime = dt; 4423 } else { 4424 this.datetime = null; 4425 } 4426 }, 4427 4428 /*
4429 highlight combos if date is invalid 4430 */ 4431 highlight: function(dt) { 4432 if(!dt.isValid()) { 4433 if(this.options.errorClass) { 4434 this.$widget.addClass(this.options.errorClass); 4435 } else { 4436 //store original border color 4437 if(!this.borderColor) { 4438 this.borderColor = this.$widget.find('select').css('border-color'); 4439 } 4440 this.$widget.find('select').css('border-color', 'red'); 4441 } 4442 } else { 4443 if(this.options.errorClass) { 4444 this.$widget.removeClass(this.options.errorClass); 4445 } else { 4446 this.$widget.find('select').css('border-color', this.borderColor); 4447 } 4448 } 4449 }, 4450 4451 leadZero: function(v) { 4452 return v <= 9 ? '0' + v : v; 4453 }, 4454 4455 destroy: function() { 4456 this.$widget.remove(); 4457 this.$element.removeData('combodate').show(); 4458 } 4459 4460 //todo: clear method 4461 }; 4462 4463 $.fn.combodate = function ( option ) { 4464 var d, args = Array.apply(null, arguments); 4465 args.shift(); 4466 4467 //getValue returns date as string / object (not jQuery object) 4468 if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) { 4469 return d.getValue.apply(d, args); 4470 } 4471 4472 return this.each(function () { 4473 var $this = $(this), 4474 data = $this.data('combodate'), 4475 options = typeof option == 'object' && option; 4476 if (!data) { 4477 $this.data('combodate', (data = new Combodate(this, options))); 4478 } 4479 if (typeof option == 'string' && typeof data[option] == 'function') { 4480 data[option].apply(data, args); 4481 } 4482 }); 4483 }; 4484 4485 $.fn.combodate.defaults = { 4486 //in this format value stored in original input 4487 format: 'DD-MM-YYYY HH:mm', 4488 //in this format items in dropdowns are displayed 4489 template: 'D / MMM / YYYY H : mm', 4490 //initial value, can be `new Date()` 4491 value: null, 4492 minYear: 1970, 4493 maxYear: new Date().getFullYear(), 4494 yearDescending: true, 4495 minuteStep: 5, 4496 secondStep: 1, 4497 firstItem: 'empty', //'name', 'empty', 'none' 4498 errorClass: null, 4499 customClass: '', 4500 roundTime: true, // whether to round minutes and seconds if step > 1 4501 smartDays: false // whether days in combo depend on selected month: 31, 30, 28 4502 }; 4503 4504}(window.jQuery)); 4505 4506/** 4507Combodate input - dropdown date and time picker. 4508Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). 4509 4510 <script src="js/moment.min.js"></script> 4511 4512Allows to input: 4513 4514* only date 4515* only time 4516* both date and time 4517 4518Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker. 4519Internally value stored as `momentjs` object. 4520 4521@class combodate 4522@extends abstractinput 4523@final 4524@since 1.4.0 4525@example 4526<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-title="Select date"></a> 4527<script> 4528$(function(){ 4529 $('#dob').editable({ 4530 format: 'YYYY-MM-DD', 4531 viewformat: 'DD.MM.YYYY', 4532 template: 'D / MMMM / YYYY', 4533 combodate: { 4534 minYear: 2000, 4535 maxYear: new Date().getFullYear(), 4536 minuteStep: 1 4537 } 4538 } 4539 }); 4540}); 4541</script> 4542**/ 4543 4544/*global moment*/ 4545 4546(function ($) { 4547 "use strict"; 4548 4549 var Constructor = function (options) { 4550 this.init('combodate', options, Constructor.defaults); 4551 4552 //by default viewformat equals to format 4553 if(!this.options.viewformat) { 4554 this.options.viewformat = this.options.format; 4555 } 4556 4557 //try parse combodate config defined as json string in data-combodate 4558 options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true); 4559 4560 //overriding combodate config (as by default jQuery extend() is not recursive) 4561 this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, { 4562 format: this.options.format, 4563 template: this.options.template 4564 }); 4565 }; 4566 4567 $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); 4568 4569 $.extend(Constructor.prototype, { 4570 render: function () { 4571 this.$input.combodate(this.options.combodate); 4572 4573 if($.fn.editableform.engine === 'bs3') { 4574 this.$input.siblings().find('select').addClass('form-control'); 4575 } 4576 4577 if(this.options.inputclass) { 4578 this.$input.siblings().find('select').addClass(this.options.inputclass); 4579 } 4580 //"clear" link 4581 /* 4582 if(this.options.clear) { 4583 this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ 4584 e.preventDefault(); 4585 e.stopPropagation(); 4586 this.clear(); 4587 }, this)); 4588 4589 this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); 4590 } 4591 */ 4592 }, 4593 4594 value2html: function(value, element) { 4595 var text = value ? value.format(this.options.viewformat) : ''; 4596 //$(element).text(text); 4597 Constructor.superclass.value2html.call(this, text, element); 4598 }, 4599 4600 html2value: function(html) { 4601 return html ? moment(html, this.options.viewformat) : null; 4602 }, 4603 4604 value2str: function(value) { 4605 return value ? value.format(this.options.format) : ''; 4606 }, 4607 4608 str2value: function(str) { 4609 return str ? moment(str, this.options.format) : null; 4610 }, 4611 4612 value2submit: function(value) { 4613 return this.value2str(value); 4614 }, 4615 4616 value2input: function(value) { 4617 this.$input.combodate('setValue', value); 4618 }, 4619 4620 input2value: function() { 4621 return this.$input.combodate('getValue', null); 4622 }, 4623 4624 activate: function() { 4625 this.$input.siblings('.combodate').find('select').eq(0).focus(); 4626 }, 4627 4628 /* 4629 clear: function() { 4630 this.$input.data('datepicker').date = null; 4631 this.$input.find('.active').removeClass('active'); 4632 }, 4633 */ 4634 4635 autosubmit: function() { 4636 4637 } 4638 4639 }); 4640 4641 Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 4642 /** 4643 @property tpl 4644 @default <input type="text"> 4645 **/ 4646 tpl:'<input type="text">', 4647 /** 4648 @property inputclass 4649 @default null 4650 **/ 4651 inputclass: null, 4652 /** 4653 Format used for sending value to server. Also applied when convert
4653ing date from <code>data-value</code> attribute.<br> 4654 See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format) 4655 4656 @property format 4657 @type string 4658 @default YYYY-MM-DD 4659 **/ 4660 format:'YYYY-MM-DD', 4661 /** 4662 Format used for displaying date. Also applied when converting date from element's text on init. 4663 If not specified equals to `format`. 4664 4665 @property viewformat 4666 @type string 4667 @default null 4668 **/ 4669 viewformat: null, 4670 /** 4671 Template used for displaying dropdowns. 4672 4673 @property template 4674 @type string 4675 @default D / MMM / YYYY 4676 **/ 4677 template: 'D / MMM / YYYY', 4678 /** 4679 Configuration of combodate. 4680 Full list of options: http://vitalets.github.com/combodate/#docs 4681 4682 @property combodate 4683 @type object 4684 @default null 4685 **/ 4686 combodate: null 4687 4688 /* 4689 (not implemented yet) 4690 Text shown as clear date button. 4691 If <code>false</code> clear button will not be rendered. 4692 4693 @property clear 4694 @type boolean|string 4695 @default 'x clear' 4696 */ 4697 //clear: '× clear' 4698 }); 4699 4700 $.fn.editabletypes.combodate = Constructor; 4701 4702}(window.jQuery)); 4703 4704/* 4705Editableform based on Twitter Bootstrap 3 4706*/ 4707(function ($) { 4708 "use strict"; 4709 4710 //store parent methods 4711 var pInitInput = $.fn.editableform.Constructor.prototype.initInput; 4712 4713 $.extend($.fn.editableform.Constructor.prototype, { 4714 initTemplate: function() { 4715 this.$form = $($.fn.editableform.template); 4716 this.$form.find('.control-group').addClass('form-group'); 4717 this.$form.find('.editable-error-block').addClass('help-block'); 4718 }, 4719 initInput: function() { 4720 pInitInput.apply(this); 4721 4722 var emptyInputClass = this.input.options.inputclass === null || this.input.options.inputclass === false; 4723 var defaultClass = 'form-control-sm'; 4724 4725 //bs3 add `form-control` class to standard inputs 4726 var stdtypes = 'text,select,textarea,password,email,url,tel,number,range,time,typeaheadjs'.split(','); 4727 if(~$.inArray(this.input.type, stdtypes)) { 4728 this.input.$input.addClass('form-control'); 4729 if(emptyInputClass) { 4730 this.input.options.inputclass = defaultClass; 4731 this.input.$input.addClass(defaultClass); 4732 } 4733 } 4734 4735 //apply size class also to buttons (to fit size of control) 4736 var $btn = this.$form.find('.editable-buttons'); 4737 var classes = emptyInputClass ? [defaultClass] : this.input.options.inputclass.split(' '); 4738 for(var i=0; i<classes.length; i++) { 4739 // `btn-sm` is default now 4740 /* 4741 if(classes[i].toLowerCase() === 'input-sm') { 4742 $btn.find('button').addClass('btn-sm'); 4743 } 4744 */ 4745 if(classes[i].toLowerCase() === 'input-lg') { 4746 $btn.find('button').removeClass('btn-sm').addClass('btn-lg'); 4747 } 4748 } 4749 } 4750 }); 4751 4752 //buttons 4753 $.fn.editableform.buttons = 4754 '<button type="submit" class="btn btn-primary btn-sm editable-submit">'+ 4755 '<i class="fa fa-check" aria-hidden="true"></i>'+ 4756 '</button>'+ 4757 '<button type="button" class="btn btn-default btn-sm editable-cancel">'+ 4758 '<i class="fa fa-times" aria-hidden="true"></i>'+ 4759 '</button>'; 4760 4761 //error classes 4762 $.fn.editableform.errorGroupClass = 'has-error'; 4763 $.fn.editableform.errorBlockClass = null; 4764 //engine 4765 $.fn.editableform.engine = 'bs4'; 4766}(window.jQuery)); 4767/** 4768* Editable Popover for Bootstrap 4 based on Popper.js 4769* --------------------- 4770* requires bootstrap-popover.js 4771*/ 4772(function ($) { 4773 "use strict"; 4774 4775 //extend methods 4776 $.extend($.fn.editableContainer.Popup.prototype, { 4777 containerName: 'popover',
4778 containerDataName: 'bs.popover', 4779 innerCss: '.popover-body', 4780 defaults: bootstrap.Popover.Default, 4781 4782 initContainer: function(){ 4783 $.extend(this.containerOptions, { 4784 trigger: 'manual', 4785 selector: false, 4786 content: ' ', 4787 template: this.defaults.template 4788 }); 4789 4790 //as template property is used in inputs, hide it from popover 4791 var t; 4792 if(this.$element.data('template')) { 4793 t = this.$element.data('template'); 4794 this.$element.removeData('template'); 4795 } 4796 4797 this.call(this.containerOptions); 4798 4799 if(t) { 4800 //restore data('template') 4801 this.$element.data('template', t); 4802 } 4803 }, 4804 4805 /* show */ 4806 innerShow: function () { 4807 this.call('show'); 4808 }, 4809 4810 /* hide */ 4811 innerHide: function () { 4812 this.call('hide'); 4813 }, 4814 4815 /* destroy */ 4816 innerDestroy: function() { 4817 this.call('dispose'); 4818 }, 4819 4820 setContainerOption: function(key, value) { 4821 this.container().options[key] = value; 4822 }, 4823 4824 setPosition: function () { 4825 (function() {}).call(this.container()); 4826 }, 4827 4828 tip: function() { 4829 return this.container() ? $(this.container().tip) : null; 4830 } 4831 }); 4832 4833}(window.jQuery)); 4834 4835/* ========================================================= 4836 * bootstrap-datepicker.js 4837 * http://www.eyecon.ro/bootstrap-datepicker 4838 * ========================================================= 4839 * Copyright 2012 Stefan Petre 4840 * Improvements by Andrew Rowls 4841 * 4842 * Licensed under the Apache License, Version 2.0 (the "License"); 4843 * you may not use this file except in compliance with the License. 4844 * You may obtain a copy of the License at 4845 * 4846 * http://www.apache.org/licenses/LICENSE-2.0 4847 * 4848 * Unless required by applicable law or agreed to in writing, software 4849 * distributed under the License is distributed on an "AS IS" BASIS, 4850 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 4851 * See the License for the specific language governing permissions and 4852 * limitations under the License. 4853 * ========================================================= */ 4854 4855(function( $ ) { 4856 4857 function UTCDate(){ 4858 return new Date(Date.UTC.apply(Date, arguments)); 4859 } 4860 function UTCToday(){ 4861 var today = new Date(); 4862 return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); 4863 } 4864 4865 // Picker object 4866 4867 var Datepicker = function(element, options) { 4868 var that = this; 4869 4870 this._process_options(options); 4871 4872 this.element = $(element); 4873 this.isInline = false; 4874 this.isInput = this.element.is('input'); 4875 this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false; 4876 this.hasInput = this.component && this.element.find('input').length; 4877 if(this.component && this.component.length === 0) 4878 this.component = false; 4879 4880 this.picker = $(DPGlobal.template); 4881 this._buildEvents(); 4882 this._attachEvents(); 4883 4884 if(this.isInline) { 4885 this.picker.addClass('datepicker-inline').appendTo(this.element); 4886 } else { 4887 this.picker.addClass('datepicker-dropdown dropdown-menu'); 4888 } 4889 4890 if (this.o.rtl){ 4891 this.picker.addClass('datepicker-rtl'); 4892 this.picker.find('.prev i, .next i') 4893 .toggleClass('icon-arrow-left icon-arrow-right'); 4894 } 4895 4896 4897 this.viewMode = this.o.startView; 4898 4899 if (this.o.calendarWeeks) 4900 this.picker.find('tfoot th.today') 4901 .attr('colspan', function(i, val){ 4902 return parseInt(val) + 1; 4903 }); 4904 4905 this._allow_update = false; 4906 4907 this.setStartDate(this.o.startDate); 4908 this.setEndDate(this.o.endDate); 4909 this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled); 4910 4911 this.fillDow(); 4912 this.fillMonths(); 4913 4914 this._allow_update = true; 4915 4916 this.update(); 4917 this.showMode(); 4918 4919 if(this.isInline) { 4920 this.show(); 4921 } 4922 }; 4923 4924 Datepicker.prototype = { 4925 constructor: Datepicker, 4926 4927 _process_options: function(opts){ 4928 // Store raw options for reference 4929 this._o = $.extend({}, this._o, opts); 4930 // Processed options 4931 var o = this.o = $.extend({}, this._o); 4932 4933 // Check if "de-DE" style date is available, if not language should 4934 // fallback to 2 letter code eg "de" 4935 var lang = o.language; 4936 if (!dates[lang]) { 4937 lang = lang.split('-')[0]; 4938 if (!dates[lang]) 4939 lang = defaults.language; 4940 } 4941 o.language = lang; 4942 4943 switch(o.startView){ 4944 case 2: 4945 case 'decade': 4946 o.startView = 2; 4947 break; 4948 case 1: 4949 case 'year': 4950 o.startView = 1; 4951 break; 4952 default: 4953 o.startView = 0; 4954 } 4955 4956 switch (o.minViewMode) { 4957 case 1: 4958 case 'months': 4959 o.minViewMode = 1; 4960 break; 4961 case 2: 4962 case 'years': 4963 o.minViewMode = 2; 4964 break; 4965 default: 4966 o.minViewMode = 0; 4967 } 4968 4969 o.startView = Math.max(o.startView, o.minViewMode); 4970 4971 o.weekStart %= 7; 4972 o.weekEnd = ((o.weekStart + 6) % 7); 4973 4974 var format = DPGlobal.parseFormat(o.format) 4975 if (o.startDate !== -Infinity) { 4976 o.startDate = DPGlobal.parseDate(o.startDate, format, o.language); 4977 } 4978 if (o.endDate !== Infinity) { 4979 o.endDate = DPGlobal.parseDate(o.endDate, format, o.language); 4980 } 4981 4982 o.daysOfWeekDisabled = o.daysOfWeekDisabled||[]; 4983 if (!$.isArray(o.daysOfWeekDisabled)) 4984 o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/); 4985 o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) { 4986 return parseInt(d, 10); 4987 }); 4988 }, 4989 _events: [], 4990 _secondaryEvents: [], 4991 _applyEvents: function(evs){ 4992 for (var i=0, el, ev; i<evs.length; i++){ 4993 el = evs[i][0]; 4994 ev = evs[i][1]; 4995 el.on(ev); 4996 } 4997 }, 4998 _unapplyEvents: function(evs){ 4999 for (var i=0, el, ev; i<evs.length; i++){ 5000 el = evs[i][0]; 5001 ev = evs[i][1]; 5002 el.off(ev); 5003 } 5004 }, 5005 _buildEvents: function(){ 5006 if (this.isInput) { // single input 5007 this._events = [ 5008 [this.element, { 5009 focus: $.proxy(this.show, this), 5010 keyup: $.proxy(this.update, this), 5011 keydown: $.proxy(this.keydown, this) 5012 }] 5013 ]; 5014 } 5015 else if (this.component && this.hasInput){ // component: input + button 5016 this._events = [ 5017 // For components that are not readonly, allow keyboard nav 5018 [this.element.find('input'), { 5019 focus: $.proxy(this.show, this), 5020 keyup: $.proxy(this.update, this), 5021 keydown: $.proxy(this.keydown, this) 5022 }], 5023 [this.component, { 5024 click: $.proxy(this.show, this) 5025 }] 5026 ]; 5027 } 5028 else if (this.element.is('div')) { // inline datepicker 5029 this.isInline = true; 5030 } 5031 else { 5032 this._events = [ 5033 [this.element, { 5034 click: $.proxy(this.show, this) 5035 }] 5036 ]; 5037 } 5038 5039 this._secondaryEvents = [ 5040 [this.picker, { 5041 click: $.proxy(this.click, this) 5042 }], 5043 [$(window), { 5044 resize: $.proxy(this.place, this) 5045 }], 5046 [$(document), { 5047 mousedown: $.proxy(function (e) { 5048 // Clicked outside the datepicker, hide it 5049 if (!( 5050 this.element.is(e.target) || 5051 this.element.find(e.target).size() || 5052 this.picker.is(e.target) || 5053 this.picker.find(e.target).size() 5054 )) { 5055 this.hide(); 5056 } 5057 }, this) 5058 }] 5059 ]; 5060 }, 5061 _attachEvents: function(){ 5062 this._detachEvents(); 5063 this._applyEvents(this._events); 5064 }, 5065 _detachEvents: function(){ 5066 this._unapplyEvents(this._events); 5067 }, 5068 _attachSecondaryEvents: function(){ 5069 this._detachSecondaryEvents(); 5070 this._applyEvents(this._secondaryEvents); 5071 }, 5072 _detachSecondaryEvents: function(){ 5073 this._unapplyEvents(this._secondaryEvents); 5074 }, 5075 _trigger: function(event, altdate){ 5076 var date = altdate || this.date, 5077 local_date = new Date(date.getTime() + (date.getTimezoneOffset()*60000)); 5078 5079 this.element.trigger({ 5080 type: event, 5081 date: local_date, 5082 format: $.proxy(function(altformat){ 5083 var format = altformat || this.o.format; 5084 return DPGlobal.formatDate(date, format, this.o.language); 5085 }, this) 5086 }); 5087 }, 5088 5089 show: function(e) { 5090 if (!this.isInline) 5091 this.picker.appendTo('body'); 5092 this.picker.show(); 5093 this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); 5094 this.place(); 5095 this._attachSecondaryEvents(); 5096 if (e) { 5097 e.preventDefault(); 5098 } 5099 this._trigger('show'); 5100 }, 5101 5102 hide: function(e){ 5103 if(this.isInline) return; 5104 if (!this.picker.is(':visible')) return; 5105 this.picker.hide().detach(); 5106 this._detachSecondaryEvents(); 5107 this.viewMode = this.o.startView; 5108 this.showMode(); 5109 5110 if ( 5111 this.o.forceParse && 5112 ( 5113 this.isInput && this.element.val() || 5114 this.hasInput && this.element.find('input').val() 5115 ) 5116 ) 5117 this.setValue(); 5118 this._trigger('hide'); 5119 }, 5120 5121 remove: function() { 5122 this.hide(); 5123 this._detachEvents(); 5124 this._detachSecondaryEvents(); 5125 this.picker.remove(); 5126 delete this.element.data().datepicker; 5127 if (!this.isInput) { 5128 delete this.element.data().date; 5129 } 5130 }, 5131 5132 getDate: function() { 5133 var d = this.getUTCDate(); 5134 return new Date(d.getTime() + (d.getTimezoneOffset()*60000)); 5135 }, 5136 5137 getUTCDate: function() { 5138 return this.date; 5139 }, 5140 5141 setDate: function(d) { 5142 this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000))); 5143 }, 5144 5145 setUTCDate: function(d) { 5146 this.date = d; 5147 this.setValue(); 5148 }, 5149 5150 setValue: function() { 5151 var formatted = this.getFormattedDate(); 5152 if (!this.isInput) { 5153 if (this.component){ 5154 this.element.find('input').val(formatted); 5155 } 5156 } else { 5157 this.element.val(formatted); 5158 } 5159 }, 5160 5161 getFormattedDate: function(format) { 5162 if (format === undefined) 5163 format = this.o.format; 5164 return DPGlobal.formatDate(this.date, format, this.o.language); 5165 }, 5166 5167 setStartDate: function(startDate){ 5168 this._process_options({startDate: startDate}); 5169 this.update(); 5170 this.updateNavArrows(); 5171 }, 5172
5173 setEndDate: function(endDate){ 5174 this._process_options({endDate: endDate}); 5175 this.update(); 5176 this.updateNavArrows(); 5177 }, 5178 5179 setDaysOfWeekDisabled: function(daysOfWeekDisabled){ 5180 this._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); 5181 this.update(); 5182 this.updateNavArrows(); 5183 }, 5184 5185 place: function(){ 5186 if(this.isInline) return; 5187 var zIndex = parseInt(this.element.parents().filter(function() { 5188 return $(this).css('z-index') != 'auto'; 5189 }).first().css('z-index'))+10; 5190 var offset = this.component ? this.component.parent().offset() : this.element.offset(); 5191 var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true); 5192 this.picker.css({ 5193 top: offset.top + height, 5194 left: offset.left, 5195 zIndex: zIndex 5196 }); 5197 }, 5198 5199 _allow_update: true, 5200 update: function(){ 5201 if (!this._allow_update) return; 5202 5203 var date, fromArgs = false; 5204 if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) { 5205 date = arguments[0]; 5206 fromArgs = true; 5207 } else { 5208 date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val(); 5209 delete this.element.data().date; 5210 } 5211 5212 this.date = DPGlobal.parseDate(date, this.o.format, this.o.language); 5213 5214 if(fromArgs) this.setValue(); 5215 5216 if (this.date < this.o.startDate) { 5217 this.viewDate = new Date(this.o.startDate); 5218 } else if (this.date > this.o.endDate) { 5219 this.viewDate = new Date(this.o.endDate); 5220 } else { 5221 this.viewDate = new Date(this.date); 5222 } 5223 this.fill(); 5224 }, 5225 5226 fillDow: function(){ 5227 var dowCnt = this.o.weekStart, 5228 html = '<tr>'; 5229 if(this.o.calendarWeeks){ 5230 var cell = '<th class="cw"> </th>'; 5231 html += cell; 5232 this.picker.find('.datepicker-days thead tr:first-child').prepend(cell); 5233 } 5234 while (dowCnt < this.o.weekStart + 7) { 5235 html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>'; 5236 } 5237 html += '</tr>'; 5238 this.picker.find('.datepicker-days thead').append(html); 5239 }, 5240 5241 fillMonths: function(){ 5242 var html = '', 5243 i = 0; 5244 while (i < 12) { 5245 html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>'; 5246 } 5247 this.picker.find('.datepicker-months td').html(html); 5248 }, 5249 5250 setRange: function(range){ 5251 if (!range || !range.length) 5252 delete this.range; 5253 else 5254 this.range = $.map(range, function(d){ return d.valueOf(); }); 5255 this.fill(); 5256 }, 5257 5258 getClassNames: function(date){ 5259 var cls = [], 5260 year = this.viewDate.getUTCFullYear(), 5261 month = this.viewDate.getUTCMonth(), 5262 currentDate = this.date.valueOf(), 5263 today = new Date(); 5264 if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) { 5265 cls.push('old'); 5266 } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) { 5267 cls.push('new'); 5268 } 5269 // Compare internal UTC date with local today, not UTC today 5270 if (this.o.todayHighlight && 5271 date.getUTCFullYear() == today.getFullYear() && 5272 date.getUTCMonth() == today.getMonth() && 5273 date.getUTCDate() == today.getDate()) { 5274 cls.push('today'); 5275 } 5276 if (currentDate && date.valueOf() == currentDate) { 5277 cls.push('active'); 5278 } 5279 if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate || 5280 $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) { 5281 cls.push('disabled'); 5282 } 5283 if (this.range){ 5284 if (date > this.range[0] && date < this.range[this.range.length-1]){ 5285 cls.push('range'); 5286 } 5287 if ($.inArray(date.valueOf(), this.range) != -1){ 5288 cls.push('selected'); 5289 } 5290 } 5291 return cls; 5292 }, 5293 5294 fill: function() { 5295 var d = new Date(this.viewDate), 5296 year = d.getUTCFullYear(), 5297 month = d.getUTCMonth(), 5298 startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, 5299 startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, 5300 endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, 5301 endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, 5302 currentDate = this.date && this.date.valueOf(), 5303 tooltip; 5304 this.picker.find('.datepicker-days thead th.datepicker-switch') 5305 .text(dates[this.o.language].months[month]+' '+year); 5306 this.picker.find('tfoot th.today') 5307 .text(dates[this.o.language].today) 5308 .toggle(this.o.todayBtn !== false); 5309 this.picker.find('tfoot th.clear') 5310 .text(dates[this.o.language].clear) 5311 .toggle(this.o.clearBtn !== false); 5312 this.updateNavArrows(); 5313 this.fillMonths(); 5314 var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), 5315 day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth()); 5316 prevMonth.setUTCDate(day); 5317 prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); 5318 var nextMonth = new Date(prevMonth); 5319 nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); 5320 nextMonth = nextMonth.valueOf(); 5321 var html = []; 5322 var clsName; 5323 while(prevMonth.valueOf() < nextMonth) { 5324 if (prevMonth.getUTCDay() == this.o.weekStart) { 5325 html.push('<tr>'); 5326 if(this.o.calendarWeeks){ 5327 // ISO 8601: First week contains first thursday. 5328 // ISO also states week starts on Monday, but we can be more abstract here. 5329 var 5330 // Start of current week: based on weekstart/current date 5331 ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5), 5332 // Thursday of this week 5333 th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), 5334 // First Thursday of year, year from thursday 5335 yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5), 5336 // Calendar week: ms between thursdays, div ms per day, div 7 days 5337 calWeek = (th - yth) / 864e5 / 7 + 1; 5338 html.push('<td class="cw">'+ calWeek +'</td>'); 5339 5340 } 5341 } 5342 clsName = this.getClassNames(prevMonth); 5343 clsName.push('day'); 5344 5345 var before = this.o.beforeShowDay(prevMonth); 5346 if (before === undefined) 5347 before = {}; 5348 else if (typeof(before) === 'boolean') 5349 before = {enabled: before}; 5350 else if (typeof(before) === 'string') 5351 before = {classes: before}; 5352 if (before.enabled === false) 5353 clsName.push('disabled'); 5354 if (before.classes) 5355 clsName = clsName.concat(before.classes.split(/\s+/)); 5356 if (before.tooltip) 5357 tooltip = before.tooltip; 5358 5359 clsName = $.unique(clsName); 5360 html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>'); 5361 if (prevMonth.getUTCDay() == this.o.weekEnd) { 5362 html.push('</tr>'); 5363 } 5364 prevMonth.setUTCDate(prevMonth.getUTCDate()+1); 5365 } 5366 this.picker.find('.datepicker-days tbody').empty().append(html.join('')); 5367 var currentYear = this.date && this.date.getUTCFullYear(); 5368 5369 var months = this.picker.find('.datepicker-months') 5370 .find('th:eq(1)') 5371 .text(year) 5372 .end() 5373 .find('span').removeClass('active'); 5374 if (currentYear && currentYear == year) { 5375 months.eq(this.date.getUTCMonth()).addClass('active'); 5376 } 5377 if (year < startYear || year > endYear) { 5378 months.addClass('disabled'); 5379 } 5380 if (year == startYear) { 5381 months.slice(0, startMonth).addClass('disabled'); 5382 } 5383 if (year == endYear) { 5384 months.slice(endMonth+1).addClass('disabled'); 5385 } 5386 5387 html = ''; 5388 year = parseInt(year/10, 10) * 10; 5389 var yearCont = this.picker.find('.datepicker-years') 5390 .find('th:eq(1)') 5391 .text(year + '-' + (year + 9)) 5392 .end() 5393 .find('td'); 5394 year -= 1;
5395 for (var i = -1; i < 11; i++) { 5396 html += '<span class="year'+(i == -1 ? ' old' : i == 10 ? ' new' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>'; 5397 year += 1; 5398 } 5399 yearCont.html(html); 5400 }, 5401 5402 updateNavArrows: function() { 5403 if (!this._allow_update) return; 5404 5405 var d = new Date(this.viewDate), 5406 year = d.getUTCFullYear(), 5407 month = d.getUTCMonth(); 5408 switch (this.viewMode) { 5409 case 0: 5410 if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) { 5411 this.picker.find('.prev').css({visibility: 'hidden'}); 5412 } else { 5413 this.picker.find('.prev').css({visibility: 'visible'}); 5414 } 5415 if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) { 5416 this.picker.find('.next').css({visibility: 'hidden'}); 5417 } else { 5418 this.picker.find('.next').css({visibility: 'visible'}); 5419 } 5420 break; 5421 case 1: 5422 case 2: 5423 if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) { 5424 this.picker.find('.prev').css({visibility: 'hidden'}); 5425 } else { 5426 this.picker.find('.prev').css({visibility: 'visible'}); 5427 } 5428 if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) { 5429 this.picker.find('.next').css({visibility: 'hidden'}); 5430 } else { 5431 this.picker.find('.next').css({visibility: 'visible'}); 5432 } 5433 break; 5434 } 5435 }, 5436 5437 click: function(e) { 5438 e.preventDefault(); 5439 var target = $(e.target).closest('span, td, th'); 5440 if (target.length == 1) { 5441 switch(target[0].nodeName.toLowerCase()) { 5442 case 'th': 5443 switch(target[0].className) { 5444 case 'datepicker-switch': 5445 this.showMode(1); 5446 break; 5447 case 'prev': 5448 case 'next': 5449 var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1); 5450 switch(this.viewMode){ 5451 case 0: 5452 this.viewDate = this.moveMonth(this.viewDate, dir); 5453 break; 5454 case 1: 5455 case 2: 5456 this.viewDate = this.moveYear(this.viewDate, dir); 5457 break; 5458 } 5459 this.fill(); 5460 break; 5461 case 'today': 5462 var date = new Date(); 5463 date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); 5464 5465 this.showMode(-2); 5466 var which = this.o.todayBtn == 'linked' ? null : 'view'; 5467 this._setDate(date, which); 5468 break; 5469 case 'clear': 5470 var element; 5471 if (this.isInput) 5472 element = this.element; 5473 else if (this.component) 5474 element = this.element.find('input'); 5475 if (element) 5476 element.val("").change(); 5477 this._trigger('changeDate'); 5478 this.update(); 5479 if (this.o.autoclose) 5480 this.hide(); 5481 break; 5482 } 5483 break; 5484 case 'span': 5485 if (!target.is('.disabled')) { 5486 this.viewDate.setUTCDate(1); 5487 if (target.is('.month')) { 5488 var day = 1; 5489 var month = target.parent().find('span').index(target); 5490 var year = this.viewDate.getUTCFullYear(); 5491 this.viewDate.setUTCMonth(month); 5492 this._trigger('changeMonth', this.viewDate); 5493 if (this.o.minViewMode === 1) { 5494 this._setDate(UTCDate(year, month, day,0,0,0,0)); 5495 } 5496 } else { 5497 var year = parseInt(target.text(), 10)||0; 5498 var day = 1; 5499 var month = 0; 5500 this.viewDate.setUTCFullYear(year); 5501 this._trigger('changeYear', this.viewDate); 5502 if (this.o.minViewMode === 2) { 5503 this._setDate(UTCDate(year, month, day,0,0,0,0)); 5504 } 5505 } 5506 this.showMode(-1); 5507 this.fill(); 5508 } 5509 break; 5510 case 'td': 5511 if (target.is('.day') && !target.is('.disabled')){ 5512 var day = parseInt(target.text(), 10)||1; 5513 var year = this.viewDate.getUTCFullYear(), 5514 month = this.viewDate.getUTCMonth(); 5515 if (target.is('.old')) { 5516 if (month === 0) { 5517 month = 11; 5518 year -= 1; 5519 } else { 5520 month -= 1; 5521 } 5522 } else if (target.is('.new')) { 5523 if (month == 11) {
5524 month = 0; 5525 year += 1; 5526 } else { 5527 month += 1; 5528 } 5529 } 5530 this._setDate(UTCDate(year, month, day,0,0,0,0)); 5531 } 5532 break; 5533 } 5534 } 5535 }, 5536 5537 _setDate: function(date, which){ 5538 if (!which || which == 'date') 5539 this.date = new Date(date); 5540 if (!which || which == 'view') 5541 this.viewDate = new Date(date); 5542 this.fill(); 5543 this.setValue(); 5544 this._trigger('changeDate'); 5545 var element; 5546 if (this.isInput) { 5547 element = this.element; 5548 } else if (this.component){ 5549 element = this.element.find('input'); 5550 } 5551 if (element) { 5552 element.change(); 5553 if (this.o.autoclose && (!which || which == 'date')) { 5554 this.hide(); 5555 } 5556 } 5557 }, 5558 5559 moveMonth: function(date, dir){ 5560 if (!dir) return date; 5561 var new_date = new Date(date.valueOf()), 5562 day = new_date.getUTCDate(), 5563 month = new_date.getUTCMonth(), 5564 mag = Math.abs(dir), 5565 new_month, test; 5566 dir = dir > 0 ? 1 : -1; 5567 if (mag == 1){ 5568 test = dir == -1 5569 // If going back one month, make sure month is not current month 5570 // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) 5571 ? function(){ return new_date.getUTCMonth() == month; } 5572 // If going forward one month, make sure month is as expected 5573 // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) 5574 : function(){ return new_date.getUTCMonth() != new_month; }; 5575 new_month = month + dir; 5576 new_date.setUTCMonth(new_month); 5577 // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 5578 if (new_month < 0 || new_month > 11) 5579 new_month = (new_month + 12) % 12; 5580 } else { 5581 // For magnitudes >1, move one month at a time... 5582 for (var i=0; i<mag; i++) 5583 // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)... 5584 new_date = this.moveMonth(new_date, dir); 5585 // ...then reset the day, keeping it in the new month 5586 new_month = new_date.getUTCMonth(); 5587 new_date.setUTCDate(day); 5588 test = function(){ return new_month != new_date.getUTCMonth(); }; 5589 } 5590 // Common date-resetting loop -- if date is beyond end of month, make it 5591 // end of month 5592 while (test()){ 5593 new_date.setUTCDate(--day); 5594 new_date.setUTCMonth(new_month); 5595 } 5596 return new_date; 5597 }, 5598 5599 moveYear: function(date, dir){ 5600 return this.moveMonth(date, dir*12); 5601 }, 5602 5603 dateWithinRange: function(date){ 5604 return date >= this.o.startDate && date <= this.o.endDate; 5605 }, 5606 5607 keydown: function(e){ 5608 if (this.picker.is(':not(:visible)')){ 5609 if (e.keyCode == 27) // allow escape to hide and re-show picker 5610 this.show(); 5611 return; 5612 } 5613 var dateChanged = false, 5614 dir, day, month, 5615 newDate, newViewDate; 5616 switch(e.keyCode){ 5617 case 27: // escape 5618 this.hide(); 5619 e.preventDefault(); 5620 break; 5621 case 37: // left 5622 case 39: // right 5623 if (!this.o.keyboardNavigation) break; 5624 dir = e.keyCode == 37 ? -1 : 1; 5625 if (e.ctrlKey){ 5626 newDate = this.moveYear(this.date, dir); 5627 newViewDate = this.moveYear(this.viewDate, dir); 5628 } else if (e.shiftKey){ 5629 newDate = this.moveMonth(this.date, dir); 5630 newViewDate = this.moveMonth(this.viewDate, dir); 5631 } else { 5632 newDate = new Date(this.date); 5633 newDate.setUTCDate(this.date.getUTCDate() + dir); 5634 newViewDate = new Date(this.viewDate); 5635 newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir); 5636 } 5637 if (this.dateWithinRange(newDate)){ 5638 this.date = newDate; 5639 this.viewDate = newViewDate; 5640 this.setValue(); 5641 this.update(); 5642 e.preventDefault(); 5643 dateChanged = true; 5644 } 5645 break; 5646 case 38: // up 5647 case 40: // down 5648 if (!this.o.keyboardNavigation) break; 5649 dir = e.keyCode == 38 ? -1 : 1; 5650 if (e.ctrlKey){ 5651 newDate = this.moveYear(this.date, dir); 5652 newViewDate = this.moveYear(this.viewDate, dir); 5653 } else if (e.shiftKey){ 5654 newDate = this.moveMonth(this.date, dir); 5655 newViewDate = this.moveMonth(this.viewDate, dir); 5656 } else { 5657 newDate = new Date(this.date); 5658 newDate.setUTCDate(this.date.getUTCDate() + dir * 7); 5659 newViewDate = new Date(this.viewDate); 5660 newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7); 5661 } 5662 if (this.dateWithinRange(newDate)){ 5663 this.date = newDate; 5664 this.viewDate = newViewDate; 5665 this.setValue(); 5666 this.update(); 5667 e.preventDefault(); 5668 dateChanged = true; 5669 } 5670 break; 5671 case 13: // enter 5672 this.hide(); 5673 e.preventDefault(); 5674 break; 5675 case 9: // tab 5676 this.hide(); 5677 break; 5678 } 5679 if (dateChanged){ 5680 this._trigger('changeDate'); 5681 var element; 5682 if (this.isInput) { 5683 element = this.element; 5684 } else if (this.component){ 5685 element = this.element.find('input'); 5686 } 5687 if (element) { 5688 element.change(); 5689 } 5690 } 5691 }, 5692 5693 showMode: function(dir) { 5694 if (dir) { 5695 this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir)); 5696 } 5697 /* 5698 vitalets: fixing bug of very special conditions: 5699 jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover. 5700 Method show() does not set display css correctly and datepicker is not shown. 5701 Changed to .css('display', 'block') solve the problem. 5702 See https://github.com/vitalets/x-editable/issues/37 5703 5704 In jquery 1.7.2+ everything works fine. 5705 */ 5706 //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); 5707 this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); 5708 this.updateNavArrows(); 5709 } 5710 }; 5711 5712 var DateRangePicker = function(element, options){ 5713 this.element = $(element); 5714 this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; }); 5715 delete options.inputs; 5716 5717 $(this.inputs) 5718 .datepicker(options) 5719 .bind('changeDate', $.proxy(this.dateUpdated, this)); 5720 5721 this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); }); 5722 this.updateDates(); 5723 }; 5724 DateRangePicker.prototype = { 5725 updateDates: function(){ 5726 this.dates = $.map(this.pickers, function(i){ return i.date; }); 5727 this.updateRanges(); 5728 }, 5729 updateRanges: function(){ 5730 var range = $.map(this.dates, function(d){ return d.valueOf(); }); 5731 $.each(this.pickers, function(i, p){ 5732 p.setRange(range); 5733 }); 5734 }, 5735 dateUpdated: function(e){ 5736 var dp = $(e.target).data('datepicker'), 5737 new_date = dp.getUTCDate(), 5738 i = $.inArray(e.target, this.inputs), 5739 l = this.inputs.length; 5740 if (i == -1) return; 5741 5742 if (new_date < this.dates[i]){ 5743 // Date being moved earlier/left 5744 while (i>=0 && new_date < this.dates[i]){ 5745 this.pickers[i--].setUTCDate(new_date); 5746 } 5747 } 5748 else if (new_date > this.dates[i]){ 5749 // Date being moved later/right 5750 while (i<l && new_date > this.dates[i]){ 5751 this.pickers[i++].setUTCDate(new_date); 5752 } 5753 } 5754 this.updateDates(); 5755 }, 5756 remove: function(){ 5757 $.map(this.pickers, function(p){ p.remove(); }); 5758 delete this.element.data().datepicker; 5759 } 5760 }; 5761 5762 function opts_from_el(el, prefix){ 5763 // Derive options from element data-attrs 5764 var data = $(el).data(), 5765 out = {}, inkey, 5766 replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'), 5767 prefix = new RegExp('^' + prefix.toLowerCase()); 5768 for (var key in data) 5769 if (prefix.test(key)){ 5770 inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); }); 5771 out[inkey] = data[key]; 5772 } 5773 return out; 5774 } 5775 5776 function opts_from_locale(lang){ 5777 // Derive options from locale plugins 5778 var out = {}; 5779 // Check if "de-DE" style date is available, if not language should 5780 // fallback to 2 letter code eg "de" 5781 if (!dates[lang]) { 5782 lang = lang.split('-')[0] 5783 if (!dates[lang]) 5784 return; 5785 } 5786 var d = dates[lang]; 5787 $.each(locale_opts, function(i,k){ 5788 if (k in d) 5789 out[k] = d[k]; 5790 }); 5791 return out; 5792 } 5793 5794 var old = $.fn.datepicker; 5795 var datepicker = $.fn.datepicker = function ( option ) { 5796 var args = Array.apply(null, arguments); 5797 args.shift(); 5798 var internal_return, 5799 this_return; 5800 this.each(function () { 5801 var $this = $(this), 5802 data = $this.data('datepicker'), 5803 options = typeof option == 'object' && option; 5804 if (!data) { 5805 var elopts = opts_from_el(this, 'date'), 5806 // Preliminary otions 5807 xopts = $.extend({}, defaults, elopts, options), 5808 locopts = opts_from_locale(xopts.language), 5809 // Options priority: js args, data-attrs, locales, defaults 5810 opts = $.extend({}, defaults, locopts, elopts, options); 5811 if ($this.is('.input-daterange') || opts.inputs){ 5812 var ropts = { 5813 inputs: opts.inputs || $this.find('input').toArray() 5814 }; 5815 $this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts)))); 5816 } 5817 else{ 5818 $this.data('datepicker', (data = new Datepicker(this, opts))); 5819 } 5820 } 5821 if (typeof option == 'string' && typeof data[option] == 'function') { 5822 internal_return = data[option].apply(data, args); 5823 if (internal_return !== undefined) 5824 return false;
5825 } 5826 }); 5827 if (internal_return !== undefined) 5828 return internal_return; 5829 else 5830 return this; 5831 }; 5832 5833 var defaults = $.fn.datepicker.defaults = { 5834 autoclose: false, 5835 beforeShowDay: $.noop, 5836 calendarWeeks: false, 5837 clearBtn: false, 5838 daysOfWeekDisabled: [], 5839 endDate: Infinity, 5840 forceParse: true, 5841 format: 'mm/dd/yyyy', 5842 keyboardNavigation: true, 5843 language: 'en', 5844 minViewMode: 0, 5845 rtl: false, 5846 startDate: -Infinity, 5847 startView: 0, 5848 todayBtn: false, 5849 todayHighlight: false, 5850 weekStart: 0 5851 }; 5852 var locale_opts = $.fn.datepicker.locale_opts = [ 5853 'format', 5854 'rtl', 5855 'weekStart' 5856 ]; 5857 $.fn.datepicker.Constructor = Datepicker; 5858 var dates = $.fn.datepicker.dates = { 5859 en: { 5860 days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], 5861 daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 5862 daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], 5863 months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], 5864 monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], 5865 today: "Today", 5866 clear: "Clear" 5867 } 5868 }; 5869 5870 var DPGlobal = { 5871 modes: [ 5872 { 5873 clsName: 'days', 5874 navFnc: 'Month', 5875 navStep: 1 5876 }, 5877 { 5878 clsName: 'months', 5879 navFnc: 'FullYear', 5880 navStep: 1 5881 }, 5882 { 5883 clsName: 'years', 5884 navFnc: 'FullYear', 5885 navStep: 10 5886 }], 5887 isLeapYear: function (year) { 5888 return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)); 5889 }, 5890 getDaysInMonth: function (year, month) { 5891 return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 5892 }, 5893 validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, 5894 nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g, 5895 parseFormat: function(format){ 5896 // IE treats \0 as a string end in inputs (truncating the value), 5897 // so it's a bad format delimiter, anyway 5898 var separators = format.replace(this.validParts, '\0').split('\0'), 5899 parts = format.match(this.validParts); 5900 if (!separators || !separators.length || !parts || parts.length === 0){ 5901 throw new Error("Invalid date format."); 5902 } 5903 return {separators: separators, parts: parts}; 5904 }, 5905 parseDate: function(date, format, language) { 5906 if (date instanceof Date) return date; 5907 if (typeof format === 'string') 5908 format = DPGlobal.parseFormat(format); 5909 if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { 5910 var part_re = /([\-+]\d+)([dmwy])/, 5911 parts = date.match(/([\-+]\d+)([dmwy])/g), 5912 part, dir; 5913 date = new Date(); 5914 for (var i=0; i<parts.length; i++) { 5915 part = part_re.exec(parts[i]); 5916 dir = parseInt(part[1]); 5917 switch(part[2]){ 5918 case 'd': 5919 date.setUTCDate(date.getUTCDate() + dir); 5920 break; 5921 case 'm': 5922 date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir); 5923 break; 5924 case 'w': 5925 date.setUTCDate(date.getUTCDate() + dir * 7); 5926 break; 5927 case 'y': 5928 date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir); 5929 break; 5930 } 5931 } 5932 return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0); 5933 } 5934 var parts = date && date.match(this.nonpunctuation) || [], 5935 date = new Date(), 5936 parsed = {}, 5937 setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'], 5938 setters_map = { 5939 yyyy: function(d,v){ return d.setUTCFullYear(v); }, 5940 yy: function(d,v){ return d.setUTCFullYear(2000+v); }, 5941 m: function(d,v){ 5942 v -= 1; 5943 while (v<0) v += 12; 5944 v %= 12; 5945 d.setUTCMonth(v); 5946 while (d.getUTCMonth() != v) 5947 d.setUTCDate(d.getUTCDate()-1); 5948 return d; 5949 }, 5950 d: function(d,v){ return d.setUTCDate(v); } 5951 }, 5952 val, filtered, part; 5953 setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; 5954 setters_map['dd'] = setters_map['d']; 5955 date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); 5956 var fparts = format.parts.slice(); 5957 // Remove noop parts 5958 if (parts.length != fparts.length) { 5959 fparts = $(fparts).filter(function(i,p){ 5960 return $.inArray(p, setters_order) !== -1; 5961 }).toArray(); 5962 } 5963 // Process remainder 5964 if (parts.length == fparts.length) { 5965 for (var i=0, cnt = fparts.length; i < cnt; i++) { 5966 val = parseInt(parts[i], 10); 5967 part = fparts[i]; 5968 if (isNaN(val)) { 5969 switch(part) { 5970 case 'MM': 5971 filtered = $(dates[language].months).filter(function(){ 5972 var m = this.slice(0, parts[i].length), 5973 p = parts[i].slice(0, m.length); 5974 return m == p; 5975 }); 5976 val = $.inArray(filtered[0], dates[language].months) + 1; 5977 break; 5978 case 'M': 5979 filtered = $(dates[language].monthsShort).filter(function(){ 5980 var m = this.slice(0, parts[i].length), 5981 p = parts[i].slice(0, m.length); 5982 return m == p; 5983 }); 5984 val = $.inArray(filtered[0], dates[language].monthsShort) + 1; 5985 break; 5986 } 5987 } 5988 parsed[part] = val; 5989 } 5990 for (var i=0, s; i<setters_order.length; i++){ 5991 s = setters_order[i]; 5992 if (s in parsed && !isNaN(parsed[s])) 5993 setters_map[s](date, parsed[s]); 5994 } 5995 } 5996 return date; 5997 }, 5998 formatDate: function(date, format, language){ 5999 if (typeof format === 'string') 6000 format = DPGlobal.parseFormat(format); 6001 var val = { 6002 d: date.getUTCDate(), 6003 D: dates[language].daysShort[date.getUTCDay()], 6004 DD: dates[language].days[date.getUTCDay()], 6005 m: date.getUTCMonth() + 1, 6006 M: dates[language].monthsShort[date.getUTCMonth()], 6007 MM: dates[language].months[date.getUTCMonth()], 6008 yy: date.getUTCFullYear().toString().substring(2), 6009 yyyy: date.getUTCFullYear() 6010 }; 6011 val.dd = (val.d < 10 ? '0' : '') + val.d; 6012 val.mm = (val.m < 10 ? '0' : '') + val.m; 6013 var date = [], 6014 seps = $.extend([], format.separators); 6015 for (var i=0, cnt = format.parts.length; i <= cnt; i++) { 6016 if (seps.length) 6017 date.push(seps.shift()); 6018 date.push(val[format.parts[i]]); 6019 } 6020 return date.join(''); 6021 }, 6022 headTemplate: '<thead>'+ 6023 '<tr>'+ 6024 '<th class="prev"><i class="icon-arrow-left"/></th>'+ 6025 '<th colspan="5" class="datepicker-switch"></th>'+ 6026 '<th class="next"><i class="icon-arrow-right"/></th>'+ 6027 '</tr>'+ 6028 '</thead>', 6029 contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>', 6030 footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>' 6031 }; 6032 DPGlobal.template = '<div class="datepicker">'+ 6033 '<div class="datepicker-days">'+ 6034 '<table class=" table-condensed">'+ 6035 DPGlobal.headTemplate+ 6036 '<tbody></tbody>'+ 6037 DPGlobal.footTemplate+ 6038 '</table>'+ 6039 '</div>'+ 6040 '<div class="datepicker-months">'+ 6041 '<table class="table-condensed">'+ 6042 DPGlobal.headTemplate+ 6043 DPGlobal.contTemplate+ 6044 DPGlobal.footTemplate+ 6045 '</table>'+ 6046 '</div>'+ 6047 '<div class="datepicker-years">'+ 6048 '<table class="table-condensed">'+ 6049 DPGlobal.headTemplate+ 6050 DPGlobal.contTemplate+ 6051 DPGlobal.footTemplate+ 6052 '</table>'+ 6053 '</div>'+ 6054 '</div>'; 6055 6056 $.fn.datepicker.DPGlobal = DPGlobal; 6057 6058 6059 /* DATEPICKER NO CONFLICT 6060 * =================== */ 6061 6062 $.fn.datepicker.noConflict = function(){ 6063 $.fn.datepicker = old; 6064 return this; 6065 }; 6066 6067 6068 /* DATEPICKER DATA-API 6069 * ================== */ 6070 6071 $(document).on( 6072 'focus.datepicker.data-api click.datepicker.data-api',
6073 '[data-provide="datepicker"]', 6074 function(e){ 6075 var $this = $(this); 6076 if ($this.data('datepicker')) return; 6077 e.preventDefault(); 6078 // component click requires us to explicitly show it 6079 datepicker.call($this, 'show'); 6080 } 6081 ); 6082 $(function(){ 6083 //$('[data-provide="datepicker-inline"]').datepicker(); 6084 //vit: changed to support noConflict() 6085 datepicker.call($('[data-provide="datepicker-inline"]')); 6086 }); 6087 6088}( window.jQuery )); 6089 6090/** 6091Bootstrap-datepicker. 6092Description and examples: https://github.com/eternicode/bootstrap-datepicker. 6093For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales 6094and set `language` option. 6095Since 1.4.0 date has different appearance in **popup** and **inline** modes. 6096 6097@class date 6098@extends abstractinput 6099@final 6100@example 6101<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-title="Select date">15/05/1984</a> 6102<script> 6103$(function(){ 6104 $('#dob').editable({ 6105 format: 'yyyy-mm-dd', 6106 viewformat: 'dd/mm/yyyy', 6107 datepicker: { 6108 weekStart: 1 6109 } 6110 } 6111 }); 6112}); 6113</script> 6114**/ 6115(function ($) { 6116 "use strict"; 6117 6118 //store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one 6119 $.fn.bdatepicker = $.fn.datepicker.noConflict(); 6120 if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name 6121 $.fn.datepicker = $.fn.bdatepicker; 6122 } 6123 6124 var Date = function (options) { 6125 this.init('date', options, Date.defaults); 6126 this.initPicker(options, Date.defaults); 6127 }; 6128 6129 $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput); 6130 6131 $.extend(Date.prototype, { 6132 initPicker: function(options, defaults) { 6133 //'format' is set directly from settings or data-* attributes 6134 6135 //by default viewformat equals to format 6136 if(!this.options.viewformat) { 6137 this.options.viewformat = this.options.format; 6138 } 6139 6140 //try parse datepicker config defined as json string in data-datepicker 6141 options.datepicker = $.fn.editableutils.tryParseJson(options.datepicker, true); 6142 6143 //overriding datepicker config (as by default jQuery extend() is not recursive) 6144 //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only 6145 this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { 6146 format: this.options.viewformat 6147 }); 6148 6149 //language 6150 this.options.datepicker.language = this.options.datepicker.language || 'en'; 6151 6152 //store DPglobal 6153 this.dpg = $.fn.bdatepicker.DPGlobal; 6154 6155 //store parsed formats 6156 this.parsedFormat = this.dpg.parseFormat(this.options.format); 6157 this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); 6158 }, 6159 6160 render: function () { 6161 this.$input.bdatepicker(this.options.datepicker); 6162 6163 //"clear" link 6164 if(this.options.clear) { 6165 this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ 6166 e.preventDefault(); 6167 e.stopPropagation(); 6168 this.clear(); 6169 }, this)); 6170 6171 this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); 6172 } 6173 }, 6174 6175 value2html: function(value, element) { 6176 var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''; 6177 Date.superclass.value2html.call(this, text, element); 6178 }, 6179 6180 html2value: function(html) { 6181 return this.parseDate(html, this.parsedViewFormat); 6182 }, 6183 6184 value2str: function(value) { 6185 return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : ''; 6186 }, 6187 6188 str2value: function(str) { 6189 return this.parseDate(str, this.parsedFormat); 6190 }, 6191 6192 value2submit: function(value) { 6193 return this.value2str(value); 6194 }, 6195 6196 value2input: function(value) { 6197 this.$input.bdatepicker('update', value); 6198 }, 6199 6200 input2value: function() { 6201 return this.$input.data('datepicker').date; 6202 }, 6203 6204 activate: function() { 6205 }, 6206 6207 clear: function() { 6208 this.$input.data('datepicker').date = null; 6209 this.$input.find('.active').removeClass('active'); 6210 if(!this.options.showbuttons) { 6211 this.$input.closest('form').submit(); 6212 } 6213 }, 6214 6215 autosubmit: function() { 6216 this.$input.on('mouseup', '.day', function(e){ 6217 if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) { 6218 return; 6219 } 6220 var $form = $(this).closest('form'); 6221 setTimeout(function() { 6222 $form.submit(); 6223 }, 200); 6224 }); 6225 //changedate is not suitable as it triggered when showing datepicker. see #149 6226 /* 6227 this.$input.on('changeDate', function(e){ 6228 var $form = $(this).closest('form'); 6229 setTimeout(function() { 6230 $form.submit(); 6231 }, 200); 6232 }); 6233 */ 6234 }, 6235 6236 /* 6237 For incorrect date bootstrap-datepicker returns current date that is not suitable 6238 for datefield. 6239 This function returns null for incorrect date. 6240 */ 6241 parseDate: function(str, format) { 6242 var date = null, formattedBack; 6243 if(str) { 6244 date = this.dpg.parseDate(str, format, this.options.datepicker.language); 6245 if(typeof str === 'string') { 6246 formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language); 6247 if(str !== formattedBack) { 6248 date = null; 6249 } 6250 } 6251 } 6252 return date; 6253 } 6254 6255 }); 6256 6257 Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 6258 /** 6259 @property tpl 6260 @default <div></div> 6261 **/ 6262 tpl:'<div class="editable-date well"></div>', 6263 /** 6264 @property inputclass 6265 @default null 6266 **/ 6267 inputclass: null, 6268 /** 6269 Format used for sending value to server. Also applied when convert
6269ing date from <code>data-value</code> attribute.<br> 6270 Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code> 6271 6272 @property format 6273 @type string 6274 @default yyyy-mm-dd 6275 **/ 6276 format:'yyyy-mm-dd', 6277 /** 6278 Format used for displaying date. Also applied when converting date from element's text on init. 6279 If not specified equals to <code>format</code> 6280 6281 @property viewformat 6282 @type string 6283 @default null 6284 **/ 6285 viewformat: null, 6286 /** 6287 Configuration of datepicker. 6288 Full list of options: http://bootstrap-datepicker.readthedocs.org/en/latest/options.html 6289 6290 @property datepicker 6291 @type object 6292 @default { 6293 weekStart: 0, 6294 startView: 0, 6295 minViewMode: 0, 6296 autoclose: false 6297 } 6298 **/ 6299 datepicker:{ 6300 weekStart: 0, 6301 startView: 0, 6302 minViewMode: 0, 6303 autoclose: false 6304 }, 6305 /** 6306 Text shown as clear date button. 6307 If <code>false</code> clear button will not be rendered. 6308 6309 @property clear 6310 @type boolean|string 6311 @default 'x clear' 6312 **/ 6313 clear: '× clear' 6314 }); 6315 6316 $.fn.editabletypes.date = Date; 6317 6318}(window.jQuery)); 6319 6320/** 6321Bootstrap datefield input - modification for inline mode. 6322Shows normal <input type="text"> and binds popup datepicker. 6323Automatically shown in inline mode. 6324 6325@class datefield 6326@extends date 6327 6328@since 1.4.0 6329**/ 6330(function ($) { 6331 "use strict"; 6332 6333 var DateField = function (options) { 6334 this.init('datefield', options, DateField.defaults); 6335 this.initPicker(options, DateField.defaults); 6336 }; 6337 6338 $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date); 6339 6340 $.extend(DateField.prototype, { 6341 render: function () { 6342 this.$input = this.$tpl.find('input'); 6343 this.setClass(); 6344 this.setAttr('placeholder'); 6345 6346 //bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js) 6347 this.$input.bdatepicker(this.options.datepicker); 6348 6349 //need to disable original event handlers 6350 this.$input.off('focus keydown'); 6351 6352 //update value of datepicker 6353 this.$input.keyup($.proxy(function(){ 6354 this.$tpl.removeData('date'); 6355 this.$tpl.bdatepicker('update'); 6356 }, this)); 6357 6358 }, 6359 6360 value2input: function(value) { 6361 this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); 6362 this.$tpl.bdatepicker('update'); 6363 }, 6364 6365 input2value: function() { 6366 return this.html2value(this.$input.val()); 6367 }, 6368 6369 activate: function() { 6370 $.fn.editabletypes.text.prototype.activate.call(this); 6371 }, 6372 6373 autosubmit: function() { 6374 //reset autosubmit to empty 6375 } 6376 }); 6377 6378 DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, { 6379 /** 6380 @property tpl 6381 **/ 6382 tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>', 6383 /** 6384 @property inputclass 6385 @default 'input-small' 6386 **/ 6387 inputclass: 'input-small', 6388 6389 /* datepicker config */ 6390 datepicker: { 6391 weekStart: 0, 6392 startView: 0, 6393 minViewMode: 0, 6394 autoclose: true 6395 } 6396 }); 6397 6398 $.fn.editabletypes.datefield = DateField; 6399 6400}(window.jQuery)); 6401 6402/** 6403Bootstrap-datetimepicker. 6404Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker). 6405Before usage you should manually include dependent js and css: 6406 6407 <link href="css/datetimepicker.css" rel="stylesheet" type="text/css"></link> 6408 <script src="js/bootstrap-datetimepicker.js"></script> 6409 6410For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales 6411and set `language` option. 6412 6413@class datetime 6414@extends abstractinput 6415@final 6416@since 1.4.4 6417@example 6418<a href="#" id="last_seen" data-type="datetime" data-pk="1" data-url="/post" title="Select date & time">15/03/2013 12:45</a> 6419<script> 6420$(function(){ 6421 $('#last_seen').editable({ 6422 format: 'yyyy-mm-dd hh:ii', 6423 viewformat: 'dd/mm/yyyy hh:ii', 6424 datetimepicker: { 6425 weekStart: 1 6426 } 6427 } 6428 }); 6429}); 6430</script> 6431**/ 6432(function ($) { 6433 "use strict"; 6434 6435 var DateTime = function (options) { 6436 this.init('datetime', options, DateTime.defaults); 6437 this.initPicker(options, DateTime.defaults); 6438 }; 6439 6440 $.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput); 6441 6442 $.extend(DateTime.prototype, { 6443 initPicker: function(options, defaults) { 6444 //'format' is set directly from settings or data-* attributes 6445 6446 //by default viewformat equals to format 6447 if(!this.options.viewformat) { 6448 this.options.viewformat = this.options.format; 6449 } 6450 6451 //try parse datetimepicker config defined as json string in data-datetimepicker 6452 options.datetimepicker = $.fn.editableutils.tryParseJson(options.datetimepicker, true); 6453 6454 //overriding datetimepicker config (as by default jQuery extend() is not recursive) 6455 //since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only 6456 this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, { 6457 format: this.options.viewformat 6458 }); 6459 6460 //language 6461 this.options.datetimepicker.language = this.options.datetimepicker.language || 'en'; 6462 6463 //store DPglobal 6464 this.dpg = $.fn.datetimepicker.DPGlobal; 6465 6466 //store parsed formats 6467 this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType); 6468 this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType); 6469 }, 6470 6471 render: function () { 6472 this.$input.datetimepicker(this.options.datetimepicker); 6473 6474 //adjust container position when viewMode changes 6475 //see https://github.com/smalot/bootstrap-datetimepicker/pull/80 6476 this.$input.on('changeMode', function(e) { 6477 var f = $(this).closest('form').parent(); 6478 //timeout here, otherwise container changes position before form has new size 6479 setTimeout(function(){ 6480 f.triggerHandler('resize'); 6481 }, 0); 6482 }); 6483 6484 //"clear" link 6485 if(this.options.clear) { 6486 this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ 6487 e.preventDefault(); 6488 e.stopPropagation(); 6489 this.clear(); 6490 }, this)); 6491 6492 this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); 6493 } 6494 }, 6495 6496 value2html: function(value, element) { 6497 //formatDate works with UTCDate! 6498 var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : ''; 6499 if(element) { 6500 DateTime.superclass.value2html.call(this, text, element); 6501 } else { 6502 return text; 6503 } 6504 }, 6505 6506 html2value: function(html) { 6507 //parseDate return utc date! 6508 var value = this.parseDate(html, this.parsedViewFormat); 6509 return value ? this.fromUTC(value) : null; 6510 }, 6511 6512 value2str: function(value) { 6513 //formatDate works with UTCDate! 6514 return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : ''; 6515 }, 6516 6517 str2value: function(str) { 6518 //parseDate return utc date! 6519 var value = this.parseDate(str, this.parsedFormat); 6520 return value ? this.fromUTC(value) : null; 6521 }, 6522 6523 value2submit: function(value) { 6524 return this.value2str(value); 6525 }, 6526 6527 value2input: function(value) { 6528 if(value) { 6529 this.$input.data('datetimepicker').setDate(value); 6530 } 6531 }, 6532 6533 input2value: function() { 6534 //date may be cleared, in that case getDate() triggers error 6535 var dt = this.$input.data('datetimepicker'); 6536 return dt.date ? dt.getDate() : null; 6537 }, 6538 6539 activate: function() { 6540 }, 6541 6542 clear: function() { 6543 this.$input.data('datetimepicker').date = null; 6544 this.$input.find('.active').removeClass('active'); 6545 if(!this.options.showbuttons) { 6546 this.$input.closest('form').submit(); 6547 } 6548 }, 6549 6550 autosubmit: function() { 6551 this.$input.on('mouseup', '.minute', function(e){ 6552 var $form = $(this).closest('form'); 6553 setTimeout(function() { 6554 $form.submit(); 6555 }, 200); 6556 }); 6557 }, 6558 6559 //convert date from local to utc 6560 toUTC: function(value) { 6561 return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value; 6562 }, 6563 6564 //convert date from utc to local 6565 fromUTC: function(value) { 6566 return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value; 6567 }, 6568 6569 /* 6570 For incorrect date bootstrap-datetimepicker returns current date that is not suitable 6571 for datetimefield. 6572 This function returns null for incorrect date. 6573 */ 6574 parseDate: function(str, format) { 6575 var date = null, formattedBack; 6576 if(str) { 6577 date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType); 6578 if(typeof str === 'string') { 6579 formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType); 6580 if(str !== formattedBack) { 6581 date = null; 6582 } 6583 } 6584 } 6585 return date; 6586 } 6587 6588 }); 6589 6590 DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { 6591 /** 6592 @property tpl 6593 @default <div></div> 6594 **/ 6595 tpl:'<div class="editable-date well"></div>', 6596 /** 6597 @property inputclass 6598 @default null 6599 **/ 6600 inputclass: null, 6601 /** 6602 Format used for sending value to server. Also applied when convert
6602ing date from <code>data-value</code> attribute.<br> 6603 Possible tokens are: <code>d, dd, m, mm, yy, yyyy, h, i</code> 6604 6605 @property format 6606 @type string 6607 @default yyyy-mm-dd hh:ii 6608 **/ 6609 format:'yyyy-mm-dd hh:ii', 6610 formatType:'standard', 6611 /** 6612 Format used for displaying date. Also applied when converting date from element's text on init. 6613 If not specified equals to <code>format</code> 6614 6615 @property viewformat 6616 @type string 6617 @default null 6618 **/ 6619 viewformat: null, 6620 /** 6621 Configuration of datetimepicker. 6622 Full list of options: https://github.com/smalot/bootstrap-datetimepicker 6623 6624 @property datetimepicker 6625 @type object 6626 @default { } 6627 **/ 6628 datetimepicker:{ 6629 todayHighlight: false, 6630 autoclose: false 6631 }, 6632 /** 6633 Text shown as clear date button. 6634 If <code>false</code> clear button will not be rendered. 6635 6636 @property clear 6637 @type boolean|string 6638 @default 'x clear' 6639 **/ 6640 clear: '× clear' 6641 }); 6642 6643 $.fn.editabletypes.datetime = DateTime; 6644 6645}(window.jQuery)); 6646/** 6647Bootstrap datetimefield input - datetime input for inline mode. 6648Shows normal <input type="text"> and binds popup datetimepicker. 6649Automatically shown in inline mode. 6650 6651@class datetimefield 6652@extends datetime 6653 6654**/ 6655(function ($) { 6656 "use strict"; 6657 6658 var DateTimeField = function (options) { 6659 this.init('datetimefield', options, DateTimeField.defaults); 6660 this.initPicker(options, DateTimeField.defaults); 6661 }; 6662 6663 $.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime); 6664 6665 $.extend(DateTimeField.prototype, { 6666 render: function () { 6667 this.$input = this.$tpl.find('input'); 6668 this.setClass(); 6669 this.setAttr('placeholder'); 6670 6671 this.$tpl.datetimepicker(this.options.datetimepicker); 6672 6673 //need to disable original event handlers 6674 this.$input.off('focus keydown'); 6675 6676 //update value of datepicker 6677 this.$input.keyup($.proxy(function(){ 6678 this.$tpl.removeData('date'); 6679 this.$tpl.datetimepicker('update'); 6680 }, this)); 6681 6682 }, 6683 6684 value2input: function(value) { 6685 this.$input.val(this.value2html(value)); 6686 this.$tpl.datetimepicker('update'); 6687 }, 6688 6689 input2value: function() { 6690 return this.html2value(this.$input.val()); 6691 }, 6692 6693 activate: function() { 6694 $.fn.editabletypes.text.prototype.activate.call(this); 6695 }, 6696 6697 autosubmit: function() { 6698 //reset autosubmit to empty 6699 } 6700 }); 6701 6702 DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, { 6703 /** 6704 @property tpl 6705 **/ 6706 tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>', 6707 /** 6708 @property inputclass 6709 @default 'input-medium' 6710 **/ 6711 inputclass: 'input-medium', 6712 6713 /* datetimepicker config */ 6714 datetimepicker:{ 6715 todayHighlight: false, 6716 autoclose: true 6717 } 6718 }); 6719 6720 $.fn.editabletypes.datetimefield = DateTimeField; 6721 6722}(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.