/dist/bootstrap-editable/js/bootstrap-editable.js

https://github.com/mnoquiao/x-editable · JavaScript · 7007 lines · 4013 code · 769 blank · 2225 comment · 805 complexity · 42d831aa0a3b6970ec74ca6b0642ecb3 MD5 · raw file

Large files are truncated click here to view the full file

  1. /*! X-editable - v1.5.1
  2. * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
  3. * http://github.com/vitalets/x-editable
  4. * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
  5. /**
  6. Form with single input element, two buttons and two states: normal/loading.
  7. Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown.
  8. Editableform is linked with one of input types, e.g. 'text', 'select' etc.
  9. @class editableform
  10. @uses text
  11. @uses textarea
  12. **/
  13. (function ($) {
  14. "use strict";
  15. var EditableForm = function (div, options) {
  16. this.options = $.extend({}, $.fn.editableform.defaults, options);
  17. this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
  18. if(!this.options.scope) {
  19. this.options.scope = this;
  20. }
  21. //nothing shown after init
  22. };
  23. EditableForm.prototype = {
  24. constructor: EditableForm,
  25. initInput: function() { //called once
  26. //take input from options (as it is created in editable-element)
  27. this.input = this.options.input;
  28. //set initial value
  29. //todo: may be add check: typeof str === 'string' ?
  30. this.value = this.input.str2value(this.options.value);
  31. //prerender: get input.$input
  32. this.input.prerender();
  33. },
  34. initTemplate: function() {
  35. this.$form = $($.fn.editableform.template);
  36. },
  37. initButtons: function() {
  38. var $btn = this.$form.find('.editable-buttons');
  39. $btn.append($.fn.editableform.buttons);
  40. if(this.options.showbuttons === 'bottom') {
  41. $btn.addClass('editable-buttons-bottom');
  42. }
  43. },
  44. /**
  45. Renders editableform
  46. @method render
  47. **/
  48. render: function() {
  49. //init loader
  50. this.$loading = $($.fn.editableform.loading);
  51. this.$div.empty().append(this.$loading);
  52. //init form template and buttons
  53. this.initTemplate();
  54. if(this.options.showbuttons) {
  55. this.initButtons();
  56. } else {
  57. this.$form.find('.editable-buttons').remove();
  58. }
  59. //show loading state
  60. this.showLoading();
  61. //flag showing is form now saving value to server.
  62. //It is needed to wait when closing form.
  63. this.isSaving = false;
  64. /**
  65. Fired when rendering starts
  66. @event rendering
  67. @param {Object} event event object
  68. **/
  69. this.$div.triggerHandler('rendering');
  70. //init input
  71. this.initInput();
  72. //append input to form
  73. this.$form.find('div.editable-input').append(this.input.$tpl);
  74. //append form to container
  75. this.$div.append(this.$form);
  76. //render input
  77. $.when(this.input.render())
  78. .then($.proxy(function () {
  79. //setup input to submit automatically when no buttons shown
  80. if(!this.options.showbuttons) {
  81. this.input.autosubmit();
  82. }
  83. //attach 'cancel' handler
  84. this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
  85. if(this.input.error) {
  86. this.error(this.input.error);
  87. this.$form.find('.editable-submit').attr('disabled', true);
  88. this.input.$input.attr('disabled', true);
  89. //prevent form from submitting
  90. this.$form.submit(function(e){ e.preventDefault(); });
  91. } else {
  92. this.error(false);
  93. this.input.$input.removeAttr('disabled');
  94. this.$form.find('.editable-submit').removeAttr('disabled');
  95. var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value;
  96. this.input.value2input(value);
  97. //attach submit handler
  98. this.$form.submit($.proxy(this.submit, this));
  99. }
  100. /**
  101. Fired when form is rendered
  102. @event rendered
  103. @param {Object} event event object
  104. **/
  105. this.$div.triggerHandler('rendered');
  106. this.showForm();
  107. //call postrender method to perform actions required visibility of form
  108. if(this.input.postrender) {
  109. this.input.postrender();
  110. }
  111. }, this));
  112. },
  113. cancel: function() {
  114. /**
  115. Fired when form was cancelled by user
  116. @event cancel
  117. @param {Object} event event object
  118. **/
  119. this.$div.triggerHandler('cancel');
  120. },
  121. showLoading: function() {
  122. var w, h;
  123. if(this.$form) {
  124. //set loading size equal to form
  125. w = this.$form.outerWidth();
  126. h = this.$form.outerHeight();
  127. if(w) {
  128. this.$loading.width(w);
  129. }
  130. if(h) {
  131. this.$loading.height(h);
  132. }
  133. this.$form.hide();
  134. } else {
  135. //stretch loading to fill container width
  136. w = this.$loading.parent().width();
  137. if(w) {
  138. this.$loading.width(w);
  139. }
  140. }
  141. this.$loading.show();
  142. },
  143. showForm: function(activate) {
  144. this.$loading.hide();
  145. this.$form.show();
  146. if(activate !== false) {
  147. this.input.activate();
  148. }
  149. /**
  150. Fired when form is shown
  151. @event show
  152. @param {Object} event event object
  153. **/
  154. this.$div.triggerHandler('show');
  155. },
  156. error: function(msg) {
  157. var $group = this.$form.find('.control-group'),
  158. $block = this.$form.find('.editable-error-block'),
  159. lines;
  160. if(msg === false) {
  161. $group.removeClass($.fn.editableform.errorGroupClass);
  162. $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
  163. } else {
  164. //convert newline to <br> for more pretty error display
  165. if(msg) {
  166. lines = (''+msg).split('\n');
  167. for (var i = 0; i < lines.length; i++) {
  168. lines[i] = $('<div>').text(lines[i]).html();
  169. }
  170. msg = lines.join('<br>');
  171. }
  172. $group.addClass($.fn.editableform.errorGroupClass);
  173. $block.addClass($.fn.editableform.errorBlockClass).html(msg).show();
  174. }
  175. },
  176. submit: function(e) {
  177. e.stopPropagation();
  178. e.preventDefault();
  179. //get new value from input
  180. var newValue = this.input.input2value();
  181. //validation: if validate returns string or truthy value - means error
  182. //if returns object like {newValue: '...'} => submitted value is reassigned to it
  183. var error = this.validate(newValue);
  184. if ($.type(error) === 'object' && error.newValue !== undefined) {
  185. newValue = error.newValue;
  186. this.input.value2input(newValue);
  187. if(typeof error.msg === 'string') {
  188. this.error(error.msg);
  189. this.showForm();
  190. return;
  191. }
  192. } else if (error) {
  193. this.error(error);
  194. this.showForm();
  195. return;
  196. }
  197. //if value not changed --> trigger 'nochange' event and return
  198. /*jslint eqeq: true*/
  199. if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) {
  200. /*jslint eqeq: false*/
  201. /**
  202. Fired when value not changed but form is submitted. Requires savenochange = false.
  203. @event nochange
  204. @param {Object} event event object
  205. **/
  206. this.$div.triggerHandler('nochange');
  207. return;
  208. }
  209. //convert value for submitting to server
  210. var submitValue = this.input.value2submit(newValue);
  211. this.isSaving = true;
  212. //sending data to server
  213. $.when(this.save(submitValue))
  214. .done($.proxy(function(response) {
  215. this.isSaving = false;
  216. //run success callback
  217. var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null;
  218. //if success callback returns false --> keep form open and do not activate input
  219. if(res === false) {
  220. this.error(false);
  221. this.showForm(false);
  222. return;
  223. }
  224. //if success callback returns string --> keep form open, show error and activate input
  225. if(typeof res === 'string') {
  226. this.error(res);
  227. this.showForm();
  228. return;
  229. }
  230. //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
  231. //it is usefull if you want to chnage value in url-function
  232. if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
  233. newValue = res.newValue;
  234. }
  235. //clear error message
  236. this.error(false);
  237. this.value = newValue;
  238. /**
  239. Fired when form is submitted
  240. @event save
  241. @param {Object} event event object
  242. @param {Object} params additional params
  243. @param {mixed} params.newValue raw new value
  244. @param {mixed} params.submitValue submitted value as string
  245. @param {Object} params.response ajax response
  246. @example
  247. $('#form-div').on('save'), function(e, params){
  248. if(params.newValue === 'username') {...}
  249. });
  250. **/
  251. this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response});
  252. }, this))
  253. .fail($.proxy(function(xhr) {
  254. this.isSaving = false;
  255. var msg;
  256. if(typeof this.options.error === 'function') {
  257. msg = this.options.error.call(this.options.scope, xhr, newValue);
  258. } else {
  259. msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!';
  260. }
  261. this.error(msg);
  262. this.showForm();
  263. }, this));
  264. },
  265. save: function(submitValue) {
  266. //try parse composite pk defined as json string in data-pk
  267. this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true);
  268. var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk,
  269. /*
  270. send on server in following cases:
  271. 1. url is function
  272. 2. url is string AND (pk defined OR send option = always)
  273. */
  274. send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))),
  275. params;
  276. if (send) { //send to server
  277. this.showLoading();
  278. //standard params
  279. params = {
  280. name: this.options.name || '',
  281. value: submitValue,
  282. pk: pk
  283. };
  284. //additional params
  285. if(typeof this.options.params === 'function') {
  286. params = this.options.params.call(this.options.scope, params);
  287. } else {
  288. //try parse json in single quotes (from data-params attribute)
  289. this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true);
  290. $.extend(params, this.options.params);
  291. }
  292. if(typeof this.options.url === 'function') { //user's function
  293. return this.options.url.call(this.options.scope, params);
  294. } else {
  295. //send ajax to server and return deferred object
  296. return $.ajax($.extend({
  297. url : this.options.url,
  298. data : params,
  299. type : 'POST'
  300. }, this.options.ajaxOptions));
  301. }
  302. }
  303. },
  304. validate: function (value) {
  305. if (value === undefined) {
  306. value = this.value;
  307. }
  308. if (typeof this.options.validate === 'function') {
  309. return this.options.validate.call(this.options.scope, value);
  310. }
  311. },
  312. option: function(key, value) {
  313. if(key in this.options) {
  314. this.options[key] = value;
  315. }
  316. if(key === 'value') {
  317. this.setValue(value);
  318. }
  319. //do not pass option to input as it is passed in editable-element
  320. },
  321. setValue: function(value, convertStr) {
  322. if(convertStr) {
  323. this.value = this.input.str2value(value);
  324. } else {
  325. this.value = value;
  326. }
  327. //if form is visible, update input
  328. if(this.$form && this.$form.is(':visible')) {
  329. this.input.value2input(this.value);
  330. }
  331. }
  332. };
  333. /*
  334. Initialize editableform. Applied to jQuery object.
  335. @method $().editableform(options)
  336. @params {Object} options
  337. @example
  338. var $form = $('&lt;div&gt;').editableform({
  339. type: 'text',
  340. name: 'username',
  341. url: '/post',
  342. value: 'vitaliy'
  343. });
  344. //to display form you should call 'render' method
  345. $form.editableform('render');
  346. */
  347. $.fn.editableform = function (option) {
  348. var args = arguments;
  349. return this.each(function () {
  350. var $this = $(this),
  351. data = $this.data('editableform'),
  352. options = typeof option === 'object' && option;
  353. if (!data) {
  354. $this.data('editableform', (data = new EditableForm(this, options)));
  355. }
  356. if (typeof option === 'string') { //call method
  357. data[option].apply(data, Array.prototype.slice.call(args, 1));
  358. }
  359. });
  360. };
  361. //keep link to constructor to allow inheritance
  362. $.fn.editableform.Constructor = EditableForm;
  363. //defaults
  364. $.fn.editableform.defaults = {
  365. /* see also defaults for input */
  366. /**
  367. Type of input. Can be <code>text|textarea|select|date|checklist</code>
  368. @property type
  369. @type string
  370. @default 'text'
  371. **/
  372. type: 'text',
  373. /**
  374. Url for submit, e.g. <code>'/post'</code>
  375. If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
  376. @property url
  377. @type string|function
  378. @default null
  379. @example
  380. url: function(params) {
  381. var d = new $.Deferred;
  382. if(params.value === 'abc') {
  383. return d.reject('error message'); //returning error via deferred object
  384. } else {
  385. //async saving data in js model
  386. someModel.asyncSaveMethod({
  387. ...,
  388. success: function(){
  389. d.resolve();
  390. }
  391. });
  392. return d.promise();
  393. }
  394. }
  395. **/
  396. url:null,
  397. /**
  398. Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value).
  399. If defined as <code>function</code> - returned object **overwrites** original ajax data.
  400. @example
  401. params: function(params) {
  402. //originally params contain pk, name and value
  403. params.a = 1;
  404. return params;
  405. }
  406. @property params
  407. @type object|function
  408. @default null
  409. **/
  410. params:null,
  411. /**
  412. Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute
  413. @property name
  414. @type string
  415. @default null
  416. **/
  417. name: null,
  418. /**
  419. Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>.
  420. Can be calculated dynamically via function.
  421. @property pk
  422. @type string|object|function
  423. @default null
  424. **/
  425. pk: null,
  426. /**
  427. Initial value. If not defined - will be taken from element's content.
  428. For __select__ type should be defined (as it is ID of shown text).
  429. @property value
  430. @type string|object
  431. @default null
  432. **/
  433. value: null,
  434. /**
  435. Value that will be displayed in input if original field value is empty (`null|undefined|''`).
  436. @property defaultValue
  437. @type string|object
  438. @default null
  439. @since 1.4.6
  440. **/
  441. defaultValue: null,
  442. /**
  443. Strategy for sending data on server. Can be `auto|always|never`.
  444. When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally.
  445. @property send
  446. @type string
  447. @default 'auto'
  448. **/
  449. send: 'auto',
  450. /**
  451. Function for client-side validation. If returns string - means validation not passed and string showed as error.
  452. Since 1.5.1 you can modify submitted value by returning object from `validate`:
  453. `{newValue: '...'}` or `{newValue: '...', msg: '...'}`
  454. @property validate
  455. @type function
  456. @default null
  457. @example
  458. validate: function(value) {
  459. if($.trim(value) == '') {
  460. return 'This field is required';
  461. }
  462. }
  463. **/
  464. validate: null,
  465. /**
  466. Success callback. Called when value successfully sent on server and **response status = 200**.
  467. Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code>
  468. or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
  469. If it returns **string** - means error occured and string is shown as error message.
  470. If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
  471. Otherwise newValue simply rendered into element.
  472. @property success
  473. @type function
  474. @default null
  475. @example
  476. success: function(response, newValue) {
  477. if(!response.success) return response.msg;
  478. }
  479. **/
  480. success: null,
  481. /**
  482. Error callback. Called when request failed (response status != 200).
  483. Usefull when you want to parse error response and display a custom message.
  484. Must return **string** - the message to be displayed in the error block.
  485. @property error
  486. @type function
  487. @default null
  488. @since 1.4.4
  489. @example
  490. error: function(response, newValue) {
  491. if(response.status === 500) {
  492. return 'Service unavailable. Please try later.';
  493. } else {
  494. return response.responseText;
  495. }
  496. }
  497. **/
  498. error: null,
  499. /**
  500. Additional options for submit ajax request.
  501. List of values: http://api.jquery.com/jQuery.ajax
  502. @property ajaxOptions
  503. @type object
  504. @default null
  505. @since 1.1.1
  506. @example
  507. ajaxOptions: {
  508. type: 'put',
  509. dataType: 'json'
  510. }
  511. **/
  512. ajaxOptions: null,
  513. /**
  514. Where to show buttons: left(true)|bottom|false
  515. Form without buttons is auto-submitted.
  516. @property showbuttons
  517. @type boolean|string
  518. @default true
  519. @since 1.1.1
  520. **/
  521. showbuttons: true,
  522. /**
  523. Scope for callback methods (success, validate).
  524. If <code>null</code> means editableform instance itself.
  525. @property scope
  526. @type DOMElement|object
  527. @default null
  528. @since 1.2.0
  529. @private
  530. **/
  531. scope: null,
  532. /**
  533. Whether to save or cancel value when it was not changed but form was submitted
  534. @property savenochange
  535. @type boolean
  536. @default false
  537. @since 1.2.0
  538. **/
  539. savenochange: false
  540. };
  541. /*
  542. Note: following params could redefined in engine: bootstrap or jqueryui:
  543. Classes 'control-group' and 'editable-error-block' must always present!
  544. */
  545. $.fn.editableform.template = '<form class="form-inline editableform">'+
  546. '<div class="control-group">' +
  547. '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
  548. '<div class="editable-error-block"></div>' +
  549. '</div>' +
  550. '</form>';
  551. //loading div
  552. $.fn.editableform.loading = '<div class="editableform-loading"></div>';
  553. //buttons
  554. $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
  555. '<button type="button" class="editable-cancel">cancel</button>';
  556. //error class attached to control-group
  557. $.fn.editableform.errorGroupClass = null;
  558. //error class attached to editable-error-block
  559. $.fn.editableform.errorBlockClass = 'editable-error';
  560. //engine
  561. $.fn.editableform.engine = 'jquery';
  562. }(window.jQuery));
  563. /**
  564. * EditableForm utilites
  565. */
  566. (function ($) {
  567. "use strict";
  568. //utils
  569. $.fn.editableutils = {
  570. /**
  571. * classic JS inheritance function
  572. */
  573. inherit: function (Child, Parent) {
  574. var F = function() { };
  575. F.prototype = Parent.prototype;
  576. Child.prototype = new F();
  577. Child.prototype.constructor = Child;
  578. Child.superclass = Parent.prototype;
  579. },
  580. /**
  581. * set caret position in input
  582. * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
  583. */
  584. setCursorPosition: function(elem, pos) {
  585. if (elem.setSelectionRange) {
  586. elem.setSelectionRange(pos, pos);
  587. } else if (elem.createTextRange) {
  588. var range = elem.createTextRange();
  589. range.collapse(true);
  590. range.moveEnd('character', pos);
  591. range.moveStart('character', pos);
  592. range.select();
  593. }
  594. },
  595. /**
  596. * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
  597. * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
  598. * safe = true --> means no exception will be thrown
  599. * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
  600. */
  601. tryParseJson: function(s, safe) {
  602. if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
  603. if (safe) {
  604. try {
  605. /*jslint evil: true*/
  606. s = (new Function('return ' + s))();
  607. /*jslint evil: false*/
  608. } catch (e) {} finally {
  609. return s;
  610. }
  611. } else {
  612. /*jslint evil: true*/
  613. s = (new Function('return ' + s))();
  614. /*jslint evil: false*/
  615. }
  616. }
  617. return s;
  618. },
  619. /**
  620. * slice object by specified keys
  621. */
  622. sliceObj: function(obj, keys, caseSensitive /* default: false */) {
  623. var key, keyLower, newObj = {};
  624. if (!$.isArray(keys) || !keys.length) {
  625. return newObj;
  626. }
  627. for (var i = 0; i < keys.length; i++) {
  628. key = keys[i];
  629. if (obj.hasOwnProperty(key)) {
  630. newObj[key] = obj[key];
  631. }
  632. if(caseSensitive === true) {
  633. continue;
  634. }
  635. //when getting data-* attributes via $.data() it's converted to lowercase.
  636. //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
  637. //workaround is code below.
  638. keyLower = key.toLowerCase();
  639. if (obj.hasOwnProperty(keyLower)) {
  640. newObj[key] = obj[keyLower];
  641. }
  642. }
  643. return newObj;
  644. },
  645. /*
  646. exclude complex objects from $.data() before pass to config
  647. */
  648. getConfigData: function($element) {
  649. var data = {};
  650. $.each($element.data(), function(k, v) {
  651. if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
  652. data[k] = v;
  653. }
  654. });
  655. return data;
  656. },
  657. /*
  658. returns keys of object
  659. */
  660. objectKeys: function(o) {
  661. if (Object.keys) {
  662. return Object.keys(o);
  663. } else {
  664. if (o !== Object(o)) {
  665. throw new TypeError('Object.keys called on a non-object');
  666. }
  667. var k=[], p;
  668. for (p in o) {
  669. if (Object.prototype.hasOwnProperty.call(o,p)) {
  670. k.push(p);
  671. }
  672. }
  673. return k;
  674. }
  675. },
  676. /**
  677. method to escape html.
  678. **/
  679. escape: function(str) {
  680. return $('<div>').text(str).html();
  681. },
  682. /*
  683. returns array items from sourceData having value property equal or inArray of 'value'
  684. */
  685. itemsByValue: function(value, sourceData, valueProp) {
  686. if(!sourceData || value === null) {
  687. return [];
  688. }
  689. if (typeof(valueProp) !== "function") {
  690. var idKey = valueProp || 'value';
  691. valueProp = function (e) { return e[idKey]; };
  692. }
  693. var isValArray = $.isArray(value),
  694. result = [],
  695. that = this;
  696. $.each(sourceData, function(i, o) {
  697. if(o.children) {
  698. result = result.concat(that.itemsByValue(value, o.children, valueProp));
  699. } else {
  700. /*jslint eqeq: true*/
  701. if(isValArray) {
  702. if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) {
  703. result.push(o);
  704. }
  705. } else {
  706. var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o;
  707. if(value == itemValue) {
  708. result.push(o);
  709. }
  710. }
  711. /*jslint eqeq: false*/
  712. }
  713. });
  714. return result;
  715. },
  716. /*
  717. Returns input by options: type, mode.
  718. */
  719. createInput: function(options) {
  720. var TypeConstructor, typeOptions, input,
  721. type = options.type;
  722. //`date` is some kind of virtual type that is transformed to one of exact types
  723. //depending on mode and core lib
  724. if(type === 'date') {
  725. //inline
  726. if(options.mode === 'inline') {
  727. if($.fn.editabletypes.datefield) {
  728. type = 'datefield';
  729. } else if($.fn.editabletypes.dateuifield) {
  730. type = 'dateuifield';
  731. }
  732. //popup
  733. } else {
  734. if($.fn.editabletypes.date) {
  735. type = 'date';
  736. } else if($.fn.editabletypes.dateui) {
  737. type = 'dateui';
  738. }
  739. }
  740. //if type still `date` and not exist in types, replace with `combodate` that is base input
  741. if(type === 'date' && !$.fn.editabletypes.date) {
  742. type = 'combodate';
  743. }
  744. }
  745. //`datetime` should be datetimefield in 'inline' mode
  746. if(type === 'datetime' && options.mode === 'inline') {
  747. type = 'datetimefield';
  748. }
  749. //change wysihtml5 to textarea for jquery UI and plain versions
  750. if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
  751. type = 'textarea';
  752. }
  753. //create input of specified type. Input will be used for converting value, not in form
  754. if(typeof $.fn.editabletypes[type] === 'function') {
  755. TypeConstructor = $.fn.editabletypes[type];
  756. typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
  757. input = new TypeConstructor(typeOptions);
  758. return input;
  759. } else {
  760. $.error('Unknown type: '+ type);
  761. return false;
  762. }
  763. },
  764. //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr
  765. supportsTransitions: function () {
  766. var b = document.body || document.documentElement,
  767. s = b.style,
  768. p = 'transition',
  769. v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
  770. if(typeof s[p] === 'string') {
  771. return true;
  772. }
  773. // Tests for vendor specific prop
  774. p = p.charAt(0).toUpperCase() + p.substr(1);
  775. for(var i=0; i<v.length; i++) {
  776. if(typeof s[v[i] + p] === 'string') {
  777. return true;
  778. }
  779. }
  780. return false;
  781. }
  782. };
  783. }(window.jQuery));
  784. /**
  785. Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
  786. This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
  787. Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
  788. Applied as jQuery method.
  789. @class editableContainer
  790. @uses editableform
  791. **/
  792. (function ($) {
  793. "use strict";
  794. var Popup = function (element, options) {
  795. this.init(element, options);
  796. };
  797. var Inline = function (element, options) {
  798. this.init(element, options);
  799. };
  800. //methods
  801. Popup.prototype = {
  802. containerName: null, //method to call container on element
  803. containerDataName: null, //object name in element's .data()
  804. innerCss: null, //tbd in child class
  805. containerClass: 'editable-container editable-popup', //css class applied to container element
  806. defaults: {}, //container itself defaults
  807. init: function(element, options) {
  808. this.$element = $(element);
  809. //since 1.4.1 container do not use data-* directly as they already merged into options.
  810. this.options = $.extend({}, $.fn.editableContainer.defaults, options);
  811. this.splitOptions();
  812. //set scope of form callbacks to element
  813. this.formOptions.scope = this.$element[0];
  814. this.initContainer();
  815. //flag to hide container, when saving value will finish
  816. this.delayedHide = false;
  817. //bind 'destroyed' listener to destroy container when element is removed from dom
  818. this.$element.on('destroyed', $.proxy(function(){
  819. this.destroy();
  820. }, this));
  821. //attach document handler to close containers on click / escape
  822. if(!$(document).data('editable-handlers-attached')) {
  823. //close all on escape
  824. $(document).on('keyup.editable', function (e) {
  825. if (e.which === 27) {
  826. $('.editable-open').editableContainer('hide');
  827. //todo: return focus on element
  828. }
  829. });
  830. //close containers when click outside
  831. //(mousedown could be better than click, it closes everything also on drag drop)
  832. $(document).on('click.editable', function(e) {
  833. var $target = $(e.target), i,
  834. exclude_classes = ['.editable-container',
  835. '.ui-datepicker-header',
  836. '.datepicker', //in inline mode datepicker is rendered into body
  837. '.modal-backdrop',
  838. '.bootstrap-wysihtml5-insert-image-modal',
  839. '.bootstrap-wysihtml5-insert-link-modal'
  840. ];
  841. //check if element is detached. It occurs when clicking in bootstrap datepicker
  842. if (!$.contains(document.documentElement, e.target)) {
  843. return;
  844. }
  845. //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document
  846. //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199
  847. //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec
  848. if($target.is(document)) {
  849. return;
  850. }
  851. //if click inside one of exclude classes --> no nothing
  852. for(i=0; i<exclude_classes.length; i++) {
  853. if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
  854. return;
  855. }
  856. }
  857. //close all open containers (except one - target)
  858. Popup.prototype.closeOthers(e.target);
  859. });
  860. $(document).data('editable-handlers-attached', true);
  861. }
  862. },
  863. //split options on containerOptions and formOptions
  864. splitOptions: function() {
  865. this.containerOptions = {};
  866. this.formOptions = {};
  867. if(!$.fn[this.containerName]) {
  868. throw new Error(this.containerName + ' not found. Have you included corresponding js file?');
  869. }
  870. //keys defined in container defaults go to container, others go to form
  871. for(var k in this.options) {
  872. if(k in this.defaults) {
  873. this.containerOptions[k] = this.options[k];
  874. } else {
  875. this.formOptions[k] = this.options[k];
  876. }
  877. }
  878. },
  879. /*
  880. Returns jquery object of container
  881. @method tip()
  882. */
  883. tip: function() {
  884. return this.container() ? this.container().$tip : null;
  885. },
  886. /* returns container object */
  887. container: function() {
  888. var container;
  889. //first, try get it by `containerDataName`
  890. if(this.containerDataName) {
  891. if(container = this.$element.data(this.containerDataName)) {
  892. return container;
  893. }
  894. }
  895. //second, try `containerName`
  896. container = this.$element.data(this.containerName);
  897. return container;
  898. },
  899. /* call native method of underlying container, e.g. this.$element.popover('method') */
  900. call: function() {
  901. this.$element[this.containerName].apply(this.$element, arguments);
  902. },
  903. initContainer: function(){
  904. this.call(this.containerOptions);
  905. },
  906. renderForm: function() {
  907. this.$form
  908. .editableform(this.formOptions)
  909. .on({
  910. save: $.proxy(this.save, this), //click on submit button (value changed)
  911. nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
  912. cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
  913. show: $.proxy(function() {
  914. if(this.delayedHide) {
  915. this.hide(this.delayedHide.reason);
  916. this.delayedHide = false;
  917. } else {
  918. this.setPosition();
  919. }
  920. }, this), //re-position container every time form is shown (occurs each time after loading state)
  921. rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
  922. resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
  923. rendered: $.proxy(function(){
  924. /**
  925. Fired when container is shown and form is rendered (for select will wait for loading dropdown options).
  926. **Note:** Bootstrap popover has own `shown` event that now cannot be separated from x-editable's one.
  927. The workaround is to check `arguments.length` that is always `2` for x-editable.
  928. @event shown
  929. @param {Object} event event object
  930. @example
  931. $('#username').on('shown', function(e, editable) {
  932. editable.input.$input.val('overwriting value of input..');
  933. });
  934. **/
  935. /*
  936. 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.
  937. */
  938. this.$element.triggerHandler('shown', $(this.options.scope).data('editable'));
  939. }, this)
  940. })
  941. .editableform('render');
  942. },
  943. /**
  944. Shows container with form
  945. @method show()
  946. @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
  947. **/
  948. /* Note: poshytip owerwrites this method totally! */
  949. show: function (closeAll) {
  950. this.$element.addClass('editable-open');
  951. if(closeAll !== false) {
  952. //close all open containers (except this)
  953. this.closeOthers(this.$element[0]);
  954. }
  955. //show container itself
  956. this.innerShow();
  957. this.tip().addClass(this.containerClass);
  958. /*
  959. Currently, form is re-rendered on every show.
  960. The main reason is that we dont know, what will container do with content when closed:
  961. remove(), detach() or just hide() - it depends on container.
  962. Detaching form itself before hide and re-insert before show is good solution,
  963. but visually it looks ugly --> container changes size before hide.
  964. */
  965. //if form already exist - delete previous data
  966. if(this.$form) {
  967. //todo: destroy prev data!
  968. //this.$form.destroy();
  969. }
  970. this.$form = $('<div>');
  971. //insert form into container body
  972. if(this.tip().is(this.innerCss)) {
  973. //for inline container
  974. this.tip().append(this.$form);
  975. } else {
  976. this.tip().find(this.innerCss).append(this.$form);
  977. }
  978. //render form
  979. this.renderForm();
  980. },
  981. /**
  982. Hides container with form
  983. @method hide()
  984. @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
  985. **/
  986. hide: function(reason) {
  987. if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
  988. return;
  989. }
  990. //if form is saving value, schedule hide
  991. if(this.$form.data('editableform').isSaving) {
  992. this.delayedHide = {reason: reason};
  993. return;
  994. } else {
  995. this.delayedHide = false;
  996. }
  997. this.$element.removeClass('editable-open');
  998. this.innerHide();
  999. /**
  1000. Fired when container was hidden. It occurs on both save or cancel.
  1001. **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one.
  1002. The workaround is to check `arguments.length` that is always `2` for x-editable.
  1003. @event hidden
  1004. @param {object} event event object
  1005. @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|manual</code>
  1006. @example
  1007. $('#username').on('hidden', function(e, reason) {
  1008. if(reason === 'save' || reason === 'cancel') {
  1009. //auto-open next editable
  1010. $(this).closest('tr').next().find('.editable').editable('show');
  1011. }
  1012. });
  1013. **/
  1014. this.$element.triggerHandler('hidden', reason || 'manual');
  1015. },
  1016. /* internal show method. To be overwritten in child classes */
  1017. innerShow: function () {
  1018. },
  1019. /* internal hide method. To be overwritten in child classes */
  1020. innerHide: function () {
  1021. },
  1022. /**
  1023. Toggles container visibility (show / hide)
  1024. @method toggle()
  1025. @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
  1026. **/
  1027. toggle: function(closeAll) {
  1028. if(this.container() && this.tip() && this.tip().is(':visible')) {
  1029. this.hide();
  1030. } else {
  1031. this.show(closeAll);
  1032. }
  1033. },
  1034. /*
  1035. Updates the position of container when content changed.
  1036. @method setPosition()
  1037. */
  1038. setPosition: function() {
  1039. //tbd in child class
  1040. },
  1041. save: function(e, params) {
  1042. /**
  1043. Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
  1044. @event save
  1045. @param {Object} event event object
  1046. @param {Object} params additional params
  1047. @param {mixed} params.newValue submitted value
  1048. @param {Object} params.response ajax response
  1049. @example
  1050. $('#username').on('save', function(e, params) {
  1051. //assuming server response: '{success: true}'
  1052. var pk = $(this).data('editableContainer').options.pk;
  1053. if(params.response && params.response.success) {
  1054. alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
  1055. } else {
  1056. alert('error!');
  1057. }
  1058. });
  1059. **/
  1060. this.$element.triggerHandler('save', params);
  1061. //hide must be after trigger, as saving value may require methods of plugin, applied to input
  1062. this.hide('save');
  1063. },
  1064. /**
  1065. Sets new option
  1066. @method option(key, value)
  1067. @param {string} key
  1068. @param {mixed} value
  1069. **/
  1070. option: function(key, value) {
  1071. this.options[key] = value;
  1072. if(key in this.containerOptions) {
  1073. this.containerOptions[key] = value;
  1074. this.setContainerOption(key, value);
  1075. } else {
  1076. this.formOptions[key] = value;
  1077. if(this.$form) {
  1078. this.$form.editableform('option', key, value);
  1079. }
  1080. }
  1081. },
  1082. setContainerOption: function(key, value) {
  1083. this.call('option', key, value);
  1084. },
  1085. /**
  1086. Destroys the container instance
  1087. @method destroy()
  1088. **/
  1089. destroy: function() {
  1090. this.hide();
  1091. this.innerDestroy();
  1092. this.$element.off('destroyed');
  1093. this.$element.removeData('editableContainer');
  1094. },
  1095. /* to be overwritten in child classes */
  1096. innerDestroy: function() {
  1097. },
  1098. /*
  1099. Closes other containers except one related to passed element.
  1100. Other containers can be cancelled or submitted (depends on onblur option)
  1101. */
  1102. closeOthers: function(element) {
  1103. $('.editable-open').each(function(i, el){
  1104. //do nothing with passed element and it's children
  1105. if(el === element || $(el).find(element).length) {
  1106. return;
  1107. }
  1108. //otherwise cancel or submit all open containers
  1109. var $el = $(el),
  1110. ec = $el.data('editableContainer');
  1111. if(!ec) {
  1112. return;
  1113. }
  1114. if(ec.options.onblur === 'cancel') {
  1115. $el.data('editableContainer').hide('onblur');
  1116. } else if(ec.options.onblur === 'submit') {
  1117. $el.data('editableContainer').tip().find('form').submit();
  1118. }
  1119. });
  1120. },
  1121. /**
  1122. Activates input of visible container (e.g. set focus)
  1123. @method activate()
  1124. **/
  1125. activate: function() {
  1126. if(this.tip && this.tip().is(':visible') && this.$form) {
  1127. this.$form.data('editableform').input.activate();
  1128. }
  1129. }
  1130. };
  1131. /**
  1132. jQuery method to initialize editableContainer.
  1133. @method $().editableContainer(options)
  1134. @params {Object} options
  1135. @example
  1136. $('#edit').editableContainer({
  1137. type: 'text',
  1138. url: '/post',
  1139. pk: 1,
  1140. value: 'hello'
  1141. });
  1142. **/
  1143. $.fn.editableContainer = function (option) {
  1144. var args = arguments;
  1145. return this.each(function () {
  1146. var $this = $(this),
  1147. dataKey = 'editableContainer',
  1148. data = $this.data(dataKey),
  1149. options = typeof option === 'object' && option,
  1150. Constructor = (options.mode === 'inline') ? Inline : Popup;
  1151. if (!data) {
  1152. $this.data(dataKey, (data = new Constructor(this, options)));
  1153. }
  1154. if (typeof option === 'string') { //call method
  1155. data[option].apply(data, Array.prototype.slice.call(args, 1));
  1156. }
  1157. });
  1158. };
  1159. //store constructors
  1160. $.fn.editableContainer.Popup = Popup;
  1161. $.fn.editableContainer.Inline = Inline;
  1162. //defaults
  1163. $.fn.editableContainer.defaults = {
  1164. /**
  1165. Initial value of form input
  1166. @property value
  1167. @type mixed
  1168. @default null
  1169. @private
  1170. **/
  1171. value: null,
  1172. /**
  1173. Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inli…