diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/DeviceManagementService.java b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/DeviceManagementService.java index b655b7b26b..46ffac27e3 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/DeviceManagementService.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/DeviceManagementService.java @@ -32,6 +32,7 @@ import io.swagger.annotations.Tag; import org.wso2.carbon.apimgt.annotations.api.Scope; import org.wso2.carbon.apimgt.annotations.api.Scopes; import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; import org.wso2.carbon.device.mgt.common.Feature; import org.wso2.carbon.device.mgt.common.app.mgt.Application; import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; @@ -48,6 +49,7 @@ import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -134,7 +136,13 @@ import javax.ws.rs.core.Response; description = "Getting Policy Compliance Details of a Device", key = "perm:devices:compliance-data", permissions = {"/device-mgt/devices/owning-device/view"} - ) + ), + @Scope( + name = "Change device status.", + description = "Change device status.", + key = "perm:devices:change-status", + permissions = {"/device-mgt/devices/change-status"} + ), } ) @Path("/devices") @@ -1073,4 +1081,79 @@ public interface DeviceManagementService { @PathParam("id") @Size(max = 45) String id); + + @PUT + @Path("/{type}/{id}/changestatus") + @ApiOperation( + produces = MediaType.APPLICATION_JSON, + consumes = MediaType.APPLICATION_JSON, + httpMethod = "PUT", + value = "Change device status by device id.", + notes = "Returns the status of the changed device operation.", + tags = "Device Management", + extensions = { + @Extension(properties = { + @ExtensionProperty(name = Constants.SCOPE, value = "perm:devices:change-status") + }) + } + ) + @ApiResponses( + value = { + @ApiResponse( + code = 200, + message = "OK. \n Successfully changed the device status.", + response = Device.class, + responseHeaders = { + @ResponseHeader( + name = "Content-Type", + description = "The content type of the body"), + @ResponseHeader( + name = "ETag", + description = "Entity Tag of the response resource.\n" + + "Used by caches, or in conditional requests."), + @ResponseHeader( + name = "Last-Modified", + description = "Date and time the resource has been modified the last time.\n" + + "Used by caches, or in conditional requests."), + }), + @ApiResponse( + code = 304, + message = "Not Modified. Empty body because the client already has the latest " + + "version of the requested resource."), + @ApiResponse( + code = 400, + message = "Bad Request. \n Invalid request or validation error.", + response = ErrorResponse.class), + @ApiResponse( + code = 404, + message = "Not Found. \n No device is found under the provided type and id.", + response = ErrorResponse.class), + @ApiResponse( + code = 500, + message = "Internal Server Error. \n " + + "Server error occurred while retrieving information requested device.", + response = ErrorResponse.class) + }) + Response changeDeviceStatus( + @ApiParam( + name = "type", + value = "The device type, such as ios, android or windows.", + required = true) + @PathParam("type") + @Size(max = 45) + String type, + @ApiParam( + name = "id", + value = "Device id", + required = true) + @PathParam("id") + @Size(max = 45) + String id, + @ApiParam( + name = "newStatus", + value = "New status of the device.", + required = true) + @QueryParam("newStatus") + EnrolmentInfo.Status newStatus); + } diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/DeviceManagementServiceImpl.java b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/DeviceManagementServiceImpl.java index 3532fbe38f..8371c7f242 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/DeviceManagementServiceImpl.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/DeviceManagementServiceImpl.java @@ -25,6 +25,7 @@ import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.device.mgt.common.Device; import org.wso2.carbon.device.mgt.common.DeviceIdentifier; import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; import org.wso2.carbon.device.mgt.common.Feature; import org.wso2.carbon.device.mgt.common.FeatureManager; import org.wso2.carbon.device.mgt.common.PaginationRequest; @@ -60,6 +61,7 @@ import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -537,4 +539,28 @@ public class DeviceManagementServiceImpl implements DeviceManagementService { } } + @PUT + @Path("/{type}/{id}/changestatus") + public Response changeDeviceStatus(@PathParam("type") @Size(max = 45) String type, + @PathParam("id") @Size(max = 45) String id, + @QueryParam("newStatus") EnrolmentInfo.Status newsStatus) { + RequestValidationUtil.validateDeviceIdentifier(type, id); + DeviceManagementProviderService deviceManagementProviderService = + DeviceMgtAPIUtils.getDeviceManagementService(); + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(id, type); + Device persistedDevice = deviceManagementProviderService.getDevice(deviceIdentifier); + if (persistedDevice == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + boolean response = deviceManagementProviderService.changeDeviceStatus(deviceIdentifier, newsStatus); + return Response.status(Response.Status.OK).entity(response).build(); + } catch (DeviceManagementException e) { + String msg = "Error occurred while changing device status of type : " + type + " and " + + "device id : " + id; + log.error(msg); + return Response.status(Response.Status.BAD_REQUEST).entity( + new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build(); + } + } } diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderService.java b/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderService.java index 082b155658..734ef3ae1e 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderService.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderService.java @@ -307,4 +307,7 @@ public interface DeviceManagementProviderService { PolicyMonitoringManager getPolicyMonitoringManager(String deviceType); + boolean changeDeviceStatus(DeviceIdentifier deviceIdentifier, EnrolmentInfo.Status newStatus) + throws DeviceManagementException; + } diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderServiceImpl.java b/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderServiceImpl.java index a9d974b4af..45cc279073 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderServiceImpl.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/service/DeviceManagementProviderServiceImpl.java @@ -84,6 +84,8 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import static org.wso2.carbon.device.mgt.common.EnrolmentInfo.Status.ACTIVE; + public class DeviceManagementProviderServiceImpl implements DeviceManagementProviderService, PluginInitializationListener { @@ -168,7 +170,7 @@ public class DeviceManagementProviderServiceImpl implements DeviceManagementProv if (deviceManager.isClaimable(deviceIdentifier)) { device.getEnrolmentInfo().setStatus(EnrolmentInfo.Status.INACTIVE); } else { - device.getEnrolmentInfo().setStatus(EnrolmentInfo.Status.ACTIVE); + device.getEnrolmentInfo().setStatus(ACTIVE); } int tenantId = this.getTenantId(); @@ -1966,6 +1968,56 @@ public class DeviceManagementProviderServiceImpl implements DeviceManagementProv return false; } + @Override + public boolean changeDeviceStatus(DeviceIdentifier deviceIdentifier, EnrolmentInfo.Status newStatus) + throws DeviceManagementException { + boolean isDeviceUpdated = false; + Device device = getDevice(deviceIdentifier); + int deviceId = device.getId(); + EnrolmentInfo enrolmentInfo = device.getEnrolmentInfo(); + enrolmentInfo.setStatus(newStatus); + int tenantId = this.getTenantId(); + switch (newStatus) { + case ACTIVE: + updateEnrollment(deviceId, enrolmentInfo, tenantId); + break; + case INACTIVE: + updateEnrollment(deviceId, enrolmentInfo, tenantId); + break; + case REMOVED: + isDeviceUpdated = disenrollDevice(deviceIdentifier); + break; + default: + throw new DeviceManagementException("Invalid status retrieved. Status : " + newStatus); + } + return isDeviceUpdated; + } + + private boolean updateEnrollment(int deviceId, EnrolmentInfo enrolmentInfo, int tenantId) + throws DeviceManagementException { + boolean isAutoCommit = true; + try { + DeviceManagementDAOFactory.openConnection(); + isAutoCommit = DeviceManagementDAOFactory.getConnection().getAutoCommit(); + DeviceManagementDAOFactory.getConnection().setAutoCommit(true); + enrollmentDAO.updateEnrollment(deviceId, enrolmentInfo, tenantId); + } catch (SQLException e) { + throw new DeviceManagementException("Error occurred while opening a connection to the data source", e); + } catch (DeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while updating the enrollment information device for" + + "id '" + deviceId + "' ." , e); + } finally { + try { + DeviceManagementDAOFactory.getConnection().setAutoCommit(isAutoCommit); + } catch (SQLException e) { + log.error("Exception occurred while setting auto commit."); + } + DeviceManagementDAOFactory.closeConnection(); + } + return true; + } + + private int getTenantId() { return CarbonContext.getThreadLocalCarbonContext().getTenantId(); } diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/modules/business-controllers/user.js b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/modules/business-controllers/user.js index c6ed7da1bd..931625def3 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/modules/business-controllers/user.js +++ b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/modules/business-controllers/user.js @@ -577,6 +577,9 @@ var userModule = function () { if (publicMethods.isAuthorized("/permission/admin/device-mgt/platform-configs/view")) { permissions["TENANT_CONFIGURATION"] = true; } + if (publicMethods.isAuthorized("/permission/admin/device-mgt/devices/change-status")) { + permissions["CHANGE_DEVICE_STATUS"] = true; + } return permissions; }; diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.details/details.js b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.details/details.js index cd1602cd5b..045f9ffecb 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.details/details.js +++ b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.details/details.js @@ -18,6 +18,8 @@ function onRequest(context) { var log = new Log("detail.js"); + var userModule = require("/app/modules/business-controllers/user.js")["userModule"]; + var permissions = userModule.getUIPermissions(); var device = context.unit.params.device; - return {"device": device}; + return {"device": device, "permissions":permissions}; } \ No newline at end of file diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.overview-section/overview-section.hbs b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.overview-section/overview-section.hbs index 8cf8c0cfd7..f1c54965ea 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.overview-section/overview-section.hbs +++ b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.overview-section/overview-section.hbs @@ -50,12 +50,41 @@
text|textarea|select|date|checklist
+
+ @property type
+ @type string
+ @default 'text'
+ **/
+ type: 'text',
+ /**
+ Url for submit, e.g. '/post'
+ If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
+
+ @property url
+ @type string|function
+ @default null
+ @example
+ url: function(params) {
+ var d = new $.Deferred;
+ if(params.value === 'abc') {
+ return d.reject('error message'); //returning error via deferred object
+ } else {
+ //async saving data in js model
+ someModel.asyncSaveMethod({
+ ...,
+ success: function(){
+ d.resolve();
+ }
+ });
+ return d.promise();
+ }
+ }
+ **/
+ url:null,
+ /**
+ Additional params for submit. If defined as object
- it is **appended** to original ajax data (pk, name and value).
+ If defined as function
- returned object **overwrites** original ajax data.
+ @example
+ params: function(params) {
+ //originally params contain pk, name and value
+ params.a = 1;
+ return params;
+ }
+
+ @property params
+ @type object|function
+ @default null
+ **/
+ params:null,
+ /**
+ Name of field. Will be submitted on server. Can be taken from id
attribute
+
+ @property name
+ @type string
+ @default null
+ **/
+ name: null,
+ /**
+ Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. {id: 1, lang: 'en'}
.
+ Can be calculated dynamically via function.
+
+ @property pk
+ @type string|object|function
+ @default null
+ **/
+ pk: null,
+ /**
+ Initial value. If not defined - will be taken from element's content.
+ For __select__ type should be defined (as it is ID of shown text).
+
+ @property value
+ @type string|object
+ @default null
+ **/
+ value: null,
+ /**
+ Value that will be displayed in input if original field value is empty (`null|undefined|''`).
+
+ @property defaultValue
+ @type string|object
+ @default null
+ @since 1.4.6
+ **/
+ defaultValue: null,
+ /**
+ Strategy for sending data on server. Can be `auto|always|never`.
+ When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally.
+
+ @property send
+ @type string
+ @default 'auto'
+ **/
+ send: 'auto',
+ /**
+ Function for client-side validation. If returns string - means validation not passed and string showed as error.
+ Since 1.5.1 you can modify submitted value by returning object from `validate`:
+ `{newValue: '...'}` or `{newValue: '...', msg: '...'}`
+
+ @property validate
+ @type function
+ @default null
+ @example
+ validate: function(value) {
+ if($.trim(value) == '') {
+ return 'This field is required';
+ }
+ }
+ **/
+ validate: null,
+ /**
+ Success callback. Called when value successfully sent on server and **response status = 200**.
+ Usefull to work with json response. For example, if your backend response can be {success: true}
+ or {success: false, msg: "server error"}
you can check it inside this callback.
+ If it returns **string** - means error occured and string is shown as error message.
+ If it returns **object like** {newValue: <something>}
- it overwrites value, submitted by user.
+ Otherwise newValue simply rendered into element.
+
+ @property success
+ @type function
+ @default null
+ @example
+ success: function(response, newValue) {
+ if(!response.success) return response.msg;
+ }
+ **/
+ success: null,
+ /**
+ Error callback. Called when request failed (response status != 200).
+ Usefull when you want to parse error response and display a custom message.
+ Must return **string** - the message to be displayed in the error block.
+
+ @property error
+ @type function
+ @default null
+ @since 1.4.4
+ @example
+ error: function(response, newValue) {
+ if(response.status === 500) {
+ return 'Service unavailable. Please try later.';
+ } else {
+ return response.responseText;
+ }
+ }
+ **/
+ error: null,
+ /**
+ Additional options for submit ajax request.
+ List of values: http://api.jquery.com/jQuery.ajax
+
+ @property ajaxOptions
+ @type object
+ @default null
+ @since 1.1.1
+ @example
+ ajaxOptions: {
+ type: 'put',
+ dataType: 'json'
+ }
+ **/
+ ajaxOptions: null,
+ /**
+ Where to show buttons: left(true)|bottom|false
+ Form without buttons is auto-submitted.
+
+ @property showbuttons
+ @type boolean|string
+ @default true
+ @since 1.1.1
+ **/
+ showbuttons: true,
+ /**
+ Scope for callback methods (success, validate).
+ If null
means editableform instance itself.
+
+ @property scope
+ @type DOMElement|object
+ @default null
+ @since 1.2.0
+ @private
+ **/
+ scope: null,
+ /**
+ Whether to save or cancel value when it was not changed but form was submitted
+
+ @property savenochange
+ @type boolean
+ @default false
+ @since 1.2.0
+ **/
+ savenochange: false
+ };
+
+ /*
+ Note: following params could redefined in engine: bootstrap or jqueryui:
+ Classes 'control-group' and 'editable-error-block' must always present!
+ */
+ $.fn.editableform.template = '';
+
+ //loading div
+ $.fn.editableform.loading = '';
+
+ //buttons
+ $.fn.editableform.buttons = ''+
+ '';
+
+ //error class attached to control-group
+ $.fn.editableform.errorGroupClass = null;
+
+ //error class attached to editable-error-block
+ $.fn.editableform.errorBlockClass = 'editable-error';
+
+ //engine
+ $.fn.editableform.engine = 'jquery';
+}(window.jQuery));
+
+/**
+* EditableForm utilites
+*/
+(function ($) {
+ "use strict";
+
+ //utils
+ $.fn.editableutils = {
+ /**
+ * classic JS inheritance function
+ */
+ inherit: function (Child, Parent) {
+ var F = function() { };
+ F.prototype = Parent.prototype;
+ Child.prototype = new F();
+ Child.prototype.constructor = Child;
+ Child.superclass = Parent.prototype;
+ },
+
+ /**
+ * set caret position in input
+ * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
+ */
+ setCursorPosition: function(elem, pos) {
+ if (elem.setSelectionRange) {
+ elem.setSelectionRange(pos, pos);
+ } else if (elem.createTextRange) {
+ var range = elem.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', pos);
+ range.moveStart('character', pos);
+ range.select();
+ }
+ },
+
+ /**
+ * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
+ * That allows such code as:
+ * safe = true --> means no exception will be thrown
+ * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
+ */
+ tryParseJson: function(s, safe) {
+ if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
+ if (safe) {
+ try {
+ /*jslint evil: true*/
+ s = (new Function('return ' + s))();
+ /*jslint evil: false*/
+ } catch (e) {} finally {
+ return s;
+ }
+ } else {
+ /*jslint evil: true*/
+ s = (new Function('return ' + s))();
+ /*jslint evil: false*/
+ }
+ }
+ return s;
+ },
+
+ /**
+ * slice object by specified keys
+ */
+ sliceObj: function(obj, keys, caseSensitive /* default: false */) {
+ var key, keyLower, newObj = {};
+
+ if (!$.isArray(keys) || !keys.length) {
+ return newObj;
+ }
+
+ for (var i = 0; i < keys.length; i++) {
+ key = keys[i];
+ if (obj.hasOwnProperty(key)) {
+ newObj[key] = obj[key];
+ }
+
+ if(caseSensitive === true) {
+ continue;
+ }
+
+ //when getting data-* attributes via $.data() it's converted to lowercase.
+ //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
+ //workaround is code below.
+ keyLower = key.toLowerCase();
+ if (obj.hasOwnProperty(keyLower)) {
+ newObj[key] = obj[keyLower];
+ }
+ }
+
+ return newObj;
+ },
+
+ /*
+ exclude complex objects from $.data() before pass to config
+ */
+ getConfigData: function($element) {
+ var data = {};
+ $.each($element.data(), function(k, v) {
+ if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
+ data[k] = v;
+ }
+ });
+ return data;
+ },
+
+ /*
+ returns keys of object
+ */
+ objectKeys: function(o) {
+ if (Object.keys) {
+ return Object.keys(o);
+ } else {
+ if (o !== Object(o)) {
+ throw new TypeError('Object.keys called on a non-object');
+ }
+ var k=[], p;
+ for (p in o) {
+ if (Object.prototype.hasOwnProperty.call(o,p)) {
+ k.push(p);
+ }
+ }
+ return k;
+ }
+
+ },
+
+ /**
+ method to escape html.
+ **/
+ escape: function(str) {
+ return $('$().editable()
. You should subscribe on it's events (save / cancel) to get profit of it.save|cancel|onblur|nochange|undefined (=manual)
+ **/
+ hide: function(reason) {
+ if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
+ return;
+ }
+
+ //if form is saving value, schedule hide
+ if(this.$form.data('editableform').isSaving) {
+ this.delayedHide = {reason: reason};
+ return;
+ } else {
+ this.delayedHide = false;
+ }
+
+ this.$element.removeClass('editable-open');
+ this.innerHide();
+
+ /**
+ Fired when container was hidden. It occurs on both save or cancel.
+ **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one.
+ The workaround is to check `arguments.length` that is always `2` for x-editable.
+
+ @event hidden
+ @param {object} event event object
+ @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|manual
+ @example
+ $('#username').on('hidden', function(e, reason) {
+ if(reason === 'save' || reason === 'cancel') {
+ //auto-open next editable
+ $(this).closest('tr').next().find('.editable').editable('show');
+ }
+ });
+ **/
+ this.$element.triggerHandler('hidden', reason || 'manual');
+ },
+
+ /* internal show method. To be overwritten in child classes */
+ innerShow: function () {
+
+ },
+
+ /* internal hide method. To be overwritten in child classes */
+ innerHide: function () {
+
+ },
+
+ /**
+ Toggles container visibility (show / hide)
+ @method toggle()
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
+ **/
+ toggle: function(closeAll) {
+ if(this.container() && this.tip() && this.tip().is(':visible')) {
+ this.hide();
+ } else {
+ this.show(closeAll);
+ }
+ },
+
+ /*
+ Updates the position of container when content changed.
+ @method setPosition()
+ */
+ setPosition: function() {
+ //tbd in child class
+ },
+
+ save: function(e, params) {
+ /**
+ Fired when new value was submitted. You can use $(this).data('editableContainer')
inside handler to access to editableContainer instance
+
+ @event save
+ @param {Object} event event object
+ @param {Object} params additional params
+ @param {mixed} params.newValue submitted value
+ @param {Object} params.response ajax response
+ @example
+ $('#username').on('save', function(e, params) {
+ //assuming server response: '{success: true}'
+ var pk = $(this).data('editableContainer').options.pk;
+ if(params.response && params.response.success) {
+ alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
+ } else {
+ alert('error!');
+ }
+ });
+ **/
+ this.$element.triggerHandler('save', params);
+
+ //hide must be after trigger, as saving value may require methods of plugin, applied to input
+ this.hide('save');
+ },
+
+ /**
+ Sets new option
+
+ @method option(key, value)
+ @param {string} key
+ @param {mixed} value
+ **/
+ option: function(key, value) {
+ this.options[key] = value;
+ if(key in this.containerOptions) {
+ this.containerOptions[key] = value;
+ this.setContainerOption(key, value);
+ } else {
+ this.formOptions[key] = value;
+ if(this.$form) {
+ this.$form.editableform('option', key, value);
+ }
+ }
+ },
+
+ setContainerOption: function(key, value) {
+ this.call('option', key, value);
+ },
+
+ /**
+ Destroys the container instance
+ @method destroy()
+ **/
+ destroy: function() {
+ this.hide();
+ this.innerDestroy();
+ this.$element.off('destroyed');
+ this.$element.removeData('editableContainer');
+ },
+
+ /* to be overwritten in child classes */
+ innerDestroy: function() {
+
+ },
+
+ /*
+ Closes other containers except one related to passed element.
+ Other containers can be cancelled or submitted (depends on onblur option)
+ */
+ closeOthers: function(element) {
+ $('.editable-open').each(function(i, el){
+ //do nothing with passed element and it's children
+ if(el === element || $(el).find(element).length) {
+ return;
+ }
+
+ //otherwise cancel or submit all open containers
+ var $el = $(el),
+ ec = $el.data('editableContainer');
+
+ if(!ec) {
+ return;
+ }
+
+ if(ec.options.onblur === 'cancel') {
+ $el.data('editableContainer').hide('onblur');
+ } else if(ec.options.onblur === 'submit') {
+ $el.data('editableContainer').tip().find('form').submit();
+ }
+ });
+
+ },
+
+ /**
+ Activates input of visible container (e.g. set focus)
+ @method activate()
+ **/
+ activate: function() {
+ if(this.tip && this.tip().is(':visible') && this.$form) {
+ this.$form.data('editableform').input.activate();
+ }
+ }
+
+ };
+
+ /**
+ jQuery method to initialize editableContainer.
+
+ @method $().editableContainer(options)
+ @params {Object} options
+ @example
+ $('#edit').editableContainer({
+ type: 'text',
+ url: '/post',
+ pk: 1,
+ value: 'hello'
+ });
+ **/
+ $.fn.editableContainer = function (option) {
+ var args = arguments;
+ return this.each(function () {
+ var $this = $(this),
+ dataKey = 'editableContainer',
+ data = $this.data(dataKey),
+ options = typeof option === 'object' && option,
+ Constructor = (options.mode === 'inline') ? Inline : Popup;
+
+ if (!data) {
+ $this.data(dataKey, (data = new Constructor(this, options)));
+ }
+
+ if (typeof option === 'string') { //call method
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
+ }
+ });
+ };
+
+ //store constructors
+ $.fn.editableContainer.Popup = Popup;
+ $.fn.editableContainer.Inline = Inline;
+
+ //defaults
+ $.fn.editableContainer.defaults = {
+ /**
+ Initial value of form input
+
+ @property value
+ @type mixed
+ @default null
+ @private
+ **/
+ value: null,
+ /**
+ Placement of container relative to element. Can be top|right|bottom|left
. Not used for inline container.
+
+ @property placement
+ @type string
+ @default 'top'
+ **/
+ placement: 'top',
+ /**
+ Whether to hide container on save/cancel.
+
+ @property autohide
+ @type boolean
+ @default true
+ @private
+ **/
+ autohide: true,
+ /**
+ Action when user clicks outside the container. Can be cancel|submit|ignore
.
+ Setting ignore
allows to have several containers open.
+
+ @property onblur
+ @type string
+ @default 'cancel'
+ @since 1.1.1
+ **/
+ onblur: 'cancel',
+
+ /**
+ Animation speed (inline mode only)
+ @property anim
+ @type string
+ @default false
+ **/
+ anim: false,
+
+ /**
+ Mode of editable, can be `popup` or `inline`
+
+ @property mode
+ @type string
+ @default 'popup'
+ @since 1.4.0
+ **/
+ mode: 'popup'
+ };
+
+ /*
+ * workaround to have 'destroyed' event to destroy popover when element is destroyed
+ * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
+ */
+ jQuery.event.special.destroyed = {
+ remove: function(o) {
+ if (o.handler) {
+ o.handler();
+ }
+ }
+ };
+
+}(window.jQuery));
+
+/**
+* Editable Inline
+* ---------------------
+*/
+(function ($) {
+ "use strict";
+
+ //copy prototype from EditableContainer
+ //extend methods
+ $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, {
+ containerName: 'editableform',
+ innerCss: '.editable-inline',
+ containerClass: 'editable-container editable-inline', //css class applied to container element
+
+ initContainer: function(){
+ //container is element
+ this.$tip = $('');
+
+ //convert anim to miliseconds (int)
+ if(!this.options.anim) {
+ this.options.anim = 0;
+ }
+ },
+
+ splitOptions: function() {
+ //all options are passed to form
+ this.containerOptions = {};
+ this.formOptions = this.options;
+ },
+
+ tip: function() {
+ return this.$tip;
+ },
+
+ innerShow: function () {
+ this.$element.hide();
+ this.tip().insertAfter(this.$element).show();
+ },
+
+ innerHide: function () {
+ this.$tip.hide(this.options.anim, $.proxy(function() {
+ this.$element.show();
+ this.innerDestroy();
+ }, this));
+ },
+
+ innerDestroy: function() {
+ if(this.tip()) {
+ this.tip().empty().remove();
+ }
+ }
+ });
+
+}(window.jQuery));
+/**
+Makes editable any HTML element on the page. Applied as jQuery method.
+
+@class editable
+@uses editableContainer
+**/
+(function ($) {
+ "use strict";
+
+ var Editable = function (element, options) {
+ this.$element = $(element);
+ //data-* has more priority over js options: because dynamically created elements may change data-*
+ this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
+ if(this.options.selector) {
+ this.initLive();
+ } else {
+ this.init();
+ }
+
+ //check for transition support
+ if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) {
+ this.options.highlight = false;
+ }
+ };
+
+ Editable.prototype = {
+ constructor: Editable,
+ init: function () {
+ var isValueByText = false,
+ doAutotext, finalize;
+
+ //name
+ this.options.name = this.options.name || this.$element.attr('id');
+
+ //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select)
+ //also we set scope option to have access to element inside input specific callbacks (e. g. source as function)
+ this.options.scope = this.$element[0];
+ this.input = $.fn.editableutils.createInput(this.options);
+ if(!this.input) {
+ return;
+ }
+
+ //set value from settings or by element's text
+ if (this.options.value === undefined || this.options.value === null) {
+ this.value = this.input.html2value($.trim(this.$element.html()));
+ isValueByText = true;
+ } else {
+ /*
+ value can be string when received from 'data-value' attribute
+ for complext objects value can be set as json string in data-value attribute,
+ e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
+ */
+ this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
+ if(typeof this.options.value === 'string') {
+ this.value = this.input.str2value(this.options.value);
+ } else {
+ this.value = this.options.value;
+ }
+ }
+
+ //add 'editable' class to every editable element
+ this.$element.addClass('editable');
+
+ //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks
+ if(this.input.type === 'textarea') {
+ this.$element.addClass('editable-pre-wrapped');
+ }
+
+ //attach handler activating editable. In disabled mode it just prevent default action (useful for links)
+ if(this.options.toggle !== 'manual') {
+ this.$element.addClass('editable-click');
+ this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
+ //prevent following link if editable enabled
+ if(!this.options.disabled) {
+ e.preventDefault();
+ }
+
+ //stop propagation not required because in document click handler it checks event target
+ //e.stopPropagation();
+
+ if(this.options.toggle === 'mouseenter') {
+ //for hover only show container
+ this.show();
+ } else {
+ //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
+ var closeAll = (this.options.toggle !== 'click');
+ this.toggle(closeAll);
+ }
+ }, this));
+ } else {
+ this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
+ }
+
+ //if display is function it's far more convinient to have autotext = always to render correctly on init
+ //see https://github.com/vitalets/x-editable-yii/issues/34
+ if(typeof this.options.display === 'function') {
+ this.options.autotext = 'always';
+ }
+
+ //check conditions for autotext:
+ switch(this.options.autotext) {
+ case 'always':
+ doAutotext = true;
+ break;
+ case 'auto':
+ //if element text is empty and value is defined and value not generated by text --> run autotext
+ doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText;
+ break;
+ default:
+ doAutotext = false;
+ }
+
+ //depending on autotext run render() or just finilize init
+ $.when(doAutotext ? this.render() : true).then($.proxy(function() {
+ if(this.options.disabled) {
+ this.disable();
+ } else {
+ this.enable();
+ }
+ /**
+ Fired when element was initialized by `$().editable()` method.
+ Please note that you should setup `init` handler **before** applying `editable`.
+
+ @event init
+ @param {Object} event event object
+ @param {Object} editable editable instance (as here it cannot accessed via data('editable'))
+ @since 1.2.0
+ @example
+ $('#username').on('init', function(e, editable) {
+ alert('initialized ' + editable.options.name);
+ });
+ $('#username').editable();
+ **/
+ this.$element.triggerHandler('init', this);
+ }, this));
+ },
+
+ /*
+ Initializes parent element for live editables
+ */
+ initLive: function() {
+ //store selector
+ var selector = this.options.selector;
+ //modify options for child elements
+ this.options.selector = false;
+ this.options.autotext = 'never';
+ //listen toggle events
+ this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
+ var $target = $(e.target);
+ if(!$target.data('editable')) {
+ //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user)
+ //see https://github.com/vitalets/x-editable/issues/137
+ if($target.hasClass(this.options.emptyclass)) {
+ $target.empty();
+ }
+ $target.editable(this.options).trigger(e);
+ }
+ }, this));
+ },
+
+ /*
+ Renders value into element's text.
+ Can call custom display method from options.
+ Can return deferred object.
+ @method render()
+ @param {mixed} response server response (if exist) to pass into display function
+ */
+ render: function(response) {
+ //do not display anything
+ if(this.options.display === false) {
+ return;
+ }
+
+ //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
+ if(this.input.value2htmlFinal) {
+ return this.input.value2html(this.value, this.$element[0], this.options.display, response);
+ //if display method defined --> use it
+ } else if(typeof this.options.display === 'function') {
+ return this.options.display.call(this.$element[0], this.value, response);
+ //else use input's original value2html() method
+ } else {
+ return this.input.value2html(this.value, this.$element[0]);
+ }
+ },
+
+ /**
+ Enables editable
+ @method enable()
+ **/
+ enable: function() {
+ this.options.disabled = false;
+ this.$element.removeClass('editable-disabled');
+ this.handleEmpty(this.isEmpty);
+ if(this.options.toggle !== 'manual') {
+ if(this.$element.attr('tabindex') === '-1') {
+ this.$element.removeAttr('tabindex');
+ }
+ }
+ },
+
+ /**
+ Disables editable
+ @method disable()
+ **/
+ disable: function() {
+ this.options.disabled = true;
+ this.hide();
+ this.$element.addClass('editable-disabled');
+ this.handleEmpty(this.isEmpty);
+ //do not stop focus on this element
+ this.$element.attr('tabindex', -1);
+ },
+
+ /**
+ Toggles enabled / disabled state of editable element
+ @method toggleDisabled()
+ **/
+ toggleDisabled: function() {
+ if(this.options.disabled) {
+ this.enable();
+ } else {
+ this.disable();
+ }
+ },
+
+ /**
+ Sets new option
+
+ @method option(key, value)
+ @param {string|object} key option name or object with several options
+ @param {mixed} value option new value
+ @example
+ $('.editable').editable('option', 'pk', 2);
+ **/
+ option: function(key, value) {
+ //set option(s) by object
+ if(key && typeof key === 'object') {
+ $.each(key, $.proxy(function(k, v){
+ this.option($.trim(k), v);
+ }, this));
+ return;
+ }
+
+ //set option by string
+ this.options[key] = value;
+
+ //disabled
+ if(key === 'disabled') {
+ return value ? this.disable() : this.enable();
+ }
+
+ //value
+ if(key === 'value') {
+ this.setValue(value);
+ }
+
+ //transfer new option to container!
+ if(this.container) {
+ this.container.option(key, value);
+ }
+
+ //pass option to input directly (as it points to the same in form)
+ if(this.input.option) {
+ this.input.option(key, value);
+ }
+
+ },
+
+ /*
+ * set emptytext if element is empty
+ */
+ handleEmpty: function (isEmpty) {
+ //do not handle empty if we do not display anything
+ if(this.options.display === false) {
+ return;
+ }
+
+ /*
+ isEmpty may be set directly as param of method.
+ It is required when we enable/disable field and can't rely on content
+ as node content is text: "Empty" that is not empty %)
+ */
+ if(isEmpty !== undefined) {
+ this.isEmpty = isEmpty;
+ } else {
+ //detect empty
+ //for some inputs we need more smart check
+ //e.g. wysihtml5 may have $(this).data('editable')
to access to editable instance
+
+ @event save
+ @param {Object} event event object
+ @param {Object} params additional params
+ @param {mixed} params.newValue submitted value
+ @param {Object} params.response ajax response
+ @example
+ $('#username').on('save', function(e, params) {
+ alert('Saved value: ' + params.newValue);
+ });
+ **/
+ //event itself is triggered by editableContainer. Description here is only for documentation
+ },
+
+ validate: function () {
+ if (typeof this.options.validate === 'function') {
+ return this.options.validate.call(this, this.value);
+ }
+ },
+
+ /**
+ Sets new value of editable
+ @method setValue(value, convertStr)
+ @param {mixed} value new value
+ @param {boolean} convertStr whether to convert value from string to internal format
+ **/
+ setValue: function(value, convertStr, response) {
+ if(convertStr) {
+ this.value = this.input.str2value(value);
+ } else {
+ this.value = value;
+ }
+ if(this.container) {
+ this.container.option('value', this.value);
+ }
+ $.when(this.render(response))
+ .then($.proxy(function() {
+ this.handleEmpty();
+ }, this));
+ },
+
+ /**
+ Activates input of visible container (e.g. set focus)
+ @method activate()
+ **/
+ activate: function() {
+ if(this.container) {
+ this.container.activate();
+ }
+ },
+
+ /**
+ Removes editable feature from element
+ @method destroy()
+ **/
+ destroy: function() {
+ this.disable();
+
+ if(this.container) {
+ this.container.destroy();
+ }
+
+ this.input.destroy();
+
+ if(this.options.toggle !== 'manual') {
+ this.$element.removeClass('editable-click');
+ this.$element.off(this.options.toggle + '.editable');
+ }
+
+ this.$element.off("save.internal");
+
+ this.$element.removeClass('editable editable-open editable-disabled');
+ this.$element.removeData('editable');
+ }
+ };
+
+ /* EDITABLE PLUGIN DEFINITION
+ * ======================= */
+
+ /**
+ jQuery method to initialize editable element.
+
+ @method $().editable(options)
+ @params {Object} options
+ @example
+ $('#username').editable({
+ type: 'text',
+ url: '/post',
+ pk: 1
+ });
+ **/
+ $.fn.editable = function (option) {
+ //special API methods returning non-jquery object
+ var result = {}, args = arguments, datakey = 'editable';
+ switch (option) {
+ /**
+ Runs client-side validation for all matched editables
+
+ @method validate()
+ @returns {Object} validation errors map
+ @example
+ $('#username, #fullname').editable('validate');
+ // possible result:
+ {
+ username: "username is required",
+ fullname: "fullname should be minimum 3 letters length"
+ }
+ **/
+ case 'validate':
+ this.each(function () {
+ var $this = $(this), data = $this.data(datakey), error;
+ if (data && (error = data.validate())) {
+ result[data.options.name] = error;
+ }
+ });
+ return result;
+
+ /**
+ Returns current values of editable elements.
+ Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements.
+ If value of some editable is `null` or `undefined` it is excluded from result object.
+ When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object.
+
+ @method getValue()
+ @param {bool} isSingle whether to return just value of single element
+ @returns {Object} object of element names and values
+ @example
+ $('#username, #fullname').editable('getValue');
+ //result:
+ {
+ username: "superuser",
+ fullname: "John"
+ }
+ //isSingle = true
+ $('#username').editable('getValue', true);
+ //result "superuser"
+ **/
+ case 'getValue':
+ if(arguments.length === 2 && arguments[1] === true) { //isSingle = true
+ result = this.eq(0).data(datakey).value;
+ } else {
+ this.each(function () {
+ var $this = $(this), data = $this.data(datakey);
+ if (data && data.value !== undefined && data.value !== null) {
+ result[data.options.name] = data.input.value2submit(data.value);
+ }
+ });
+ }
+ return result;
+
+ /**
+ This method collects values from several editable elements and submit them all to server.
+ Internally it runs client-side validation for all fields and submits only in case of success.
+ See creating new records for details.
+ Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case
+ `url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`.
+
+ @method submit(options)
+ @param {object} options
+ @param {object} options.url url to submit data
+ @param {object} options.data additional data to submit
+ @param {object} options.ajaxOptions additional ajax options
+ @param {function} options.error(obj) error handler
+ @param {function} options.success(obj,config) success handler
+ @returns {Object} jQuery object
+ **/
+ case 'submit': //collects value, validate and submit to server for creating new record
+ var config = arguments[1] || {},
+ $elems = this,
+ errors = this.editable('validate');
+
+ // validation ok
+ if($.isEmptyObject(errors)) {
+ var ajaxOptions = {};
+
+ // for single element use url, success etc from options
+ if($elems.length === 1) {
+ var editable = $elems.data('editable');
+ //standard params
+ var params = {
+ name: editable.options.name || '',
+ value: editable.input.value2submit(editable.value),
+ pk: (typeof editable.options.pk === 'function') ?
+ editable.options.pk.call(editable.options.scope) :
+ editable.options.pk
+ };
+
+ //additional params
+ if(typeof editable.options.params === 'function') {
+ params = editable.options.params.call(editable.options.scope, params);
+ } else {
+ //try parse json in single quotes (from data-params attribute)
+ editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true);
+ $.extend(params, editable.options.params);
+ }
+
+ ajaxOptions = {
+ url: editable.options.url,
+ data: params,
+ type: 'POST'
+ };
+
+ // use success / error from options
+ config.success = config.success || editable.options.success;
+ config.error = config.error || editable.options.error;
+
+ // multiple elements
+ } else {
+ var values = this.editable('getValue');
+
+ ajaxOptions = {
+ url: config.url,
+ data: values,
+ type: 'POST'
+ };
+ }
+
+ // ajax success callabck (response 200 OK)
+ ajaxOptions.success = typeof config.success === 'function' ? function(response) {
+ config.success.call($elems, response, config);
+ } : $.noop;
+
+ // ajax error callabck
+ ajaxOptions.error = typeof config.error === 'function' ? function() {
+ config.error.apply($elems, arguments);
+ } : $.noop;
+
+ // extend ajaxOptions
+ if(config.ajaxOptions) {
+ $.extend(ajaxOptions, config.ajaxOptions);
+ }
+
+ // extra data
+ if(config.data) {
+ $.extend(ajaxOptions.data, config.data);
+ }
+
+ // perform ajax request
+ $.ajax(ajaxOptions);
+ } else { //client-side validation error
+ if(typeof config.error === 'function') {
+ config.error.call($elems, errors);
+ }
+ }
+ return this;
+ }
+
+ //return jquery object
+ return this.each(function () {
+ var $this = $(this),
+ data = $this.data(datakey),
+ options = typeof option === 'object' && option;
+
+ //for delegated targets do not store `editable` object for element
+ //it's allows several different selectors.
+ //see: https://github.com/vitalets/x-editable/issues/312
+ if(options && options.selector) {
+ data = new Editable(this, options);
+ return;
+ }
+
+ if (!data) {
+ $this.data(datakey, (data = new Editable(this, options)));
+ }
+
+ if (typeof option === 'string') { //call method
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
+ }
+ });
+ };
+
+
+ $.fn.editable.defaults = {
+ /**
+ Type of input. Can be text|textarea|select|date|checklist
and more
+
+ @property type
+ @type string
+ @default 'text'
+ **/
+ type: 'text',
+ /**
+ Sets disabled state of editable
+
+ @property disabled
+ @type boolean
+ @default false
+ **/
+ disabled: false,
+ /**
+ How to toggle editable. Can be click|dblclick|mouseenter|manual
.
+ When set to manual
you should manually call show/hide
methods of editable.
+ **Note**: if you call show
or toggle
inside **click** handler of some DOM element,
+ you need to apply e.stopPropagation()
because containers are being closed on any click on document.
+
+ @example
+ $('#edit-button').click(function(e) {
+ e.stopPropagation();
+ $('#username').editable('toggle');
+ });
+
+ @property toggle
+ @type string
+ @default 'click'
+ **/
+ toggle: 'click',
+ /**
+ Text shown when element is empty.
+
+ @property emptytext
+ @type string
+ @default 'Empty'
+ **/
+ emptytext: 'Empty',
+ /**
+ Allows to automatically set element's text based on it's value. Can be auto|always|never
. Useful for select and date.
+ For example, if dropdown list is {1: 'a', 2: 'b'}
and element's value set to 1
, it's html will be automatically set to 'a'
.
+ auto
- text will be automatically set only if element is empty.
+ always|never
- always(never) try to set element's text.
+
+ @property autotext
+ @type string
+ @default 'auto'
+ **/
+ autotext: 'auto',
+ /**
+ Initial value of input. If not set, taken from element's text.
+ Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option).
+ For example, to display currency sign:
+ @example
+
+
+
+ @property value
+ @type mixed
+ @default element's text
+ **/
+ value: null,
+ /**
+ Callback to perform custom displaying of value in element's text.
+ If `null`, default input's display used.
+ If `false`, no displaying methods will be called, element's text will never change.
+ Runs under element's scope.
+ _**Parameters:**_
+
+ * `value` current value to be displayed
+ * `response` server response (if display called after ajax submit), since 1.4.0
+
+ For _inputs with source_ (select, checklist) parameters are different:
+
+ * `value` current value to be displayed
+ * `sourceData` array of items for current input (e.g. dropdown items)
+ * `response` server response (if display called after ajax submit), since 1.4.0
+
+ To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
+
+ @property display
+ @type function|boolean
+ @default null
+ @since 1.2.0
+ @example
+ display: function(value, sourceData) {
+ //display checklist as comma-separated values
+ var html = [],
+ checked = $.fn.editableutils.itemsByValue(value, sourceData);
+
+ if(checked.length) {
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
+ $(this).html(html.join(', '));
+ } else {
+ $(this).empty();
+ }
+ }
+ **/
+ display: null,
+ /**
+ Css class applied when editable text is empty.
+
+ @property emptyclass
+ @type string
+ @since 1.4.1
+ @default editable-empty
+ **/
+ emptyclass: 'editable-empty',
+ /**
+ Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
+ You may set it to `null` if you work with editables locally and submit them together.
+
+ @property unsavedclass
+ @type string
+ @since 1.4.1
+ @default editable-unsaved
+ **/
+ unsavedclass: 'editable-unsaved',
+ /**
+ If selector is provided, editable will be delegated to the specified targets.
+ Usefull for dynamically generated DOM elements.
+ **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options,
+ as they actually become editable only after first click.
+ You should manually set class `editable-click` to these elements.
+ Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element:
+
+ @property selector
+ @type string
+ @since 1.4.1
+ @default null
+ @example
+
+
+
+ **/
+ selector: null,
+ /**
+ Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers.
+
+ @property highlight
+ @type string|boolean
+ @since 1.4.5
+ @default #FFFF80
+ **/
+ highlight: '#FFFF80'
+ };
+
+}(window.jQuery));
+
+/**
+AbstractInput - base class for all editable inputs.
+It defines interface to be implemented by any input type.
+To create your own input you can inherit from this class.
+
+@class abstractinput
+**/
+(function ($) {
+ "use strict";
+
+ //types
+ $.fn.editabletypes = {};
+
+ var AbstractInput = function () { };
+
+ AbstractInput.prototype = {
+ /**
+ Initializes input
+
+ @method init()
+ **/
+ init: function(type, options, defaults) {
+ this.type = type;
+ this.options = $.extend({}, defaults, options);
+ },
+
+ /*
+ this method called before render to init $tpl that is inserted in DOM
+ */
+ prerender: function() {
+ this.$tpl = $(this.options.tpl); //whole tpl as jquery object
+ this.$input = this.$tpl; //control itself, can be changed in render method
+ this.$clear = null; //clear button
+ this.error = null; //error message, if input cannot be rendered
+ },
+
+ /**
+ Renders input from tpl. Can return jQuery deferred object.
+ Can be overwritten in child objects
+
+ @method render()
+ **/
+ render: function() {
+
+ },
+
+ /**
+ Sets element's html by value.
+
+ @method value2html(value, element)
+ @param {mixed} value
+ @param {DOMElement} element
+ **/
+ value2html: function(value, element) {
+ $(element)[this.options.escape ? 'text' : 'html']($.trim(value));
+ },
+
+ /**
+ Converts element's html to value
+
+ @method html2value(html)
+ @param {string} html
+ @returns {mixed}
+ **/
+ html2value: function(html) {
+ return $('true
and source is **string url** - results will be cached for fields with the same source.
+ Usefull for editable column in grid to prevent extra requests.
+
+ @property sourceCache
+ @type boolean
+ @default true
+ @since 1.2.0
+ **/
+ sourceCache: true,
+ /**
+ Additional ajax options to be used in $.ajax() when loading list from server.
+ Useful to send extra parameters (`data` key) or change request method (`type` key).
+
+ @property sourceOptions
+ @type object|function
+ @default null
+ @since 1.5.0
+ **/
+ sourceOptions: null
+ });
+
+ $.fn.editabletypes.list = List;
+
+}(window.jQuery));
+
+/**
+Text input
+
+@class text
+@extends abstractinput
+@final
+@example
+awesome
+
+**/
+(function ($) {
+ "use strict";
+
+ var Text = function (options) {
+ this.init('text', options, Text.defaults);
+ };
+
+ $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
+
+ $.extend(Text.prototype, {
+ render: function() {
+ this.renderClear();
+ this.setClass();
+ this.setAttr('placeholder');
+ },
+
+ activate: function() {
+ if(this.$input.is(':visible')) {
+ this.$input.focus();
+ $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
+ if(this.toggleClear) {
+ this.toggleClear();
+ }
+ }
+ },
+
+ //render clear button
+ renderClear: function() {
+ if (this.options.clear) {
+ this.$clear = $('');
+ this.$input.after(this.$clear)
+ .css('padding-right', 24)
+ .keyup($.proxy(function(e) {
+ //arrows, enter, tab, etc
+ if(~$.inArray(e.keyCode, [40,38,9,13,27])) {
+ return;
+ }
+
+ clearTimeout(this.t);
+ var that = this;
+ this.t = setTimeout(function() {
+ that.toggleClear(e);
+ }, 100);
+
+ }, this))
+ .parent().css('position', 'relative');
+
+ this.$clear.click($.proxy(this.clear, this));
+ }
+ },
+
+ postrender: function() {
+ /*
+ //now `clear` is positioned via css
+ if(this.$clear) {
+ //can position clear button only here, when form is shown and height can be calculated
+// var h = this.$input.outerHeight(true) || 20,
+ var h = this.$clear.parent().height(),
+ delta = (h - this.$clear.height()) / 2;
+
+ //this.$clear.css({bottom: delta, right: delta});
+ }
+ */
+ },
+
+ //show / hide clear button
+ toggleClear: function(e) {
+ if(!this.$clear) {
+ return;
+ }
+
+ var len = this.$input.val().length,
+ visible = this.$clear.is(':visible');
+
+ if(len && !visible) {
+ this.$clear.show();
+ }
+
+ if(!len && visible) {
+ this.$clear.hide();
+ }
+ },
+
+ clear: function() {
+ this.$clear.hide();
+ this.$input.val('').focus();
+ }
+ });
+
+ Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
+ /**
+ @property tpl
+ @default
+ **/
+ tpl: '',
+ /**
+ Placeholder attribute of input. Shown when input is empty.
+
+ @property placeholder
+ @type string
+ @default null
+ **/
+ placeholder: null,
+
+ /**
+ Whether to show `clear` button
+
+ @property clear
+ @type boolean
+ @default true
+ **/
+ clear: true
+ });
+
+ $.fn.editabletypes.text = Text;
+
+}(window.jQuery));
+
+/**
+Textarea input
+
+@class textarea
+@extends abstractinput
+@final
+@example
+awesome comment!
+
+**/
+(function ($) {
+ "use strict";
+
+ var Textarea = function (options) {
+ this.init('textarea', options, Textarea.defaults);
+ };
+
+ $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
+
+ $.extend(Textarea.prototype, {
+ render: function () {
+ this.setClass();
+ this.setAttr('placeholder');
+ this.setAttr('rows');
+
+ //ctrl + enter
+ this.$input.keydown(function (e) {
+ if (e.ctrlKey && e.which === 13) {
+ $(this).closest('form').submit();
+ }
+ });
+ },
+
+ //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant!
+ /*
+ value2html: function(value, element) {
+ var html = '', lines;
+ if(value) {
+ lines = value.split("\n");
+ for (var i = 0; i < lines.length; i++) {
+ lines[i] = $('