Ext.ns("Terrasoft.model"); /** * Base class of the model */ Ext.define("Terrasoft.model.BaseModel", { extend: "Terrasoft.core.BaseObject", alternateClassName: "Terrasoft.BaseModel", /** * Link to the composite model object * @private * @type {Object} */ model: null, /** * Values passed to the configuration object during initialization * @protected * @type {Object} */ values: null, /** * Methods added to the model instance during initialization. Transmitted as a configuration object * @protected * @type {Object} */ methods: null, /** * Collection of the model bindings. * @private * @type {Terrasoft.Collection} */ bindMap: null, /** * A sign that the model is in a dependency search state. In this mode, all the properties of the model, * the value of which is requested for reading is written to the array {@link #dependentProperties}. * @private * @type {Boolean} */ isTracking: false, /** * Columns of the model, which may include fields from the root schema, linked schemas, as well as calculated fields * @type {Object} */ columns: null, /** * Array of Modified Model Fields * @type {Array} */ changedValues: null, /** * An object of dependent properties, which is populated when the observation of dependency search is enabled * {@link #setTrackingState}. * @type {Object} */ dependentProperties: null, /** * A sign that the model supports validation * @type {Boolean} */ canValidate: false, /** * The prefix for the modification of the presentation model attribute. * @private * @type {String} */ columnChangeEventPrefix: "change:", /** * @private */ _canDefineAttributeProperty: function (propertyName) { return !Terrasoft.contains(propertyName, ".") && !this.hasOwnProperty(propertyName); }, /** * @private */ _getDefinePropertyName: function (attributeName) { return "$" + attributeName; }, /** * @private */ _getDefineAttributes: function (attributeName) { var attributes = {}; if (Ext.isObject(attributeName)) { attributes = attributeName; } else { attributes[attributeName] = {}; } return attributes; }, /** * @private */ _defineAttributeProperty: function (attributeName) { var attributes = this._getDefineAttributes(attributeName); _.keys(attributes).forEach(function (key) { var propertyName = this._getDefinePropertyName(key); if (!this._canDefineAttributeProperty(propertyName)) { return; } Object.defineProperty(this, propertyName, { get: function () { return this.get(key); }, set: function (value) { this.set(key, value); } }); }, this); }, /** * @private */ _defineAttributesProperties: function () { _.keys(this.columns).forEach(this._defineAttributeProperty, this); }, /** * @inheritdoc Terrasoft.BaseObject#constructor * @override */ constructor: function (config) { this.callParent(arguments); this.model = new Backbone.Model(); this.initModelColumns(); this.setInitialValue(); if (config) { this.loadFromColumnValues(config.values, config.rowConfig); Ext.apply(this, config.methods); } this.model.on("change", this.onDataChange, this); this.addEvents("change"); this._defineAttributesProperties(); }, /** * Initialize model columns. * @protected */ initModelColumns: function () { var columns = this.getModelColumns(); this.columns = columns; }, /** * Returns model columns. If columns property is null, returns empty object. * @protected * @return {Object} Model columns. */ getModelColumns: function () { return this.columns || {}; }, /** * Loads data from columns, where each property is a column with a value * @protected */ setInitialValue: function () { var columns = this.columns; Terrasoft.each(columns, function (column, columnName) { if (column.value !== undefined) { this.set(columnName, column.value, null); } }, this); }, /** * Initializes the configuration of the columns and loads data from the object, where each property represents * column with value * @param {Object} values Object of Values */ loadFromColumnValues: function (values) { if (values) { Terrasoft.each(values, function (column, columnName) { this.set(columnName, column, { silent: true }); }, this); } }, /** * Subscribe to the model event. You can subscribe both to changes in all properties of the model, and to change * of one or several properties. * @override * @param {String} event Event name * @param {Function} handler Handlers * @param {Object} scope Context */ on: function (event, handler, scope) { if (!this.events[event]) { this.model.on(event, handler, scope); } else { this.callParent(arguments); } }, /** * Unsubscribe from the model event * @override * @param {String} event Event name * @param {Function} handler Handlers * @param {Object} scope Context */ un: function (event, handler, scope) { if (!this.events[event]) { this.model.off(event, handler, scope); } else { this.callParent(arguments); } }, /** * Model data change handler * @protected * @param {Object} obj Object with changed data * @param {Object} options The object of additional parameters with which the data modification method was called */ onDataChange: function (obj, options) { var changedValues = this.changedValues = this.changedValues || {}; for (var key in obj.changed) { changedValues[key] = obj.changed[key]; } this.fireEvent("change", this, options, obj && obj.changed); }, /** * Reverts changes for specified attribute. * @param {String} key Name of the attribute. */ revertAttributeChanges: function (key) { var changedValues = this.changedValues; if (changedValues.hasOwnProperty(key)) { var previousValue = this.model.previous(key); this.set(key, previousValue); delete this.changedValues[key]; } }, /** * Subscribes to changes to the presentation model attribute. * @protected * @param {String} columnName The name of the attribute of the view model. * @param {Function} handler An event handler for the modification of the presentation model attribute. * @param {Object} [scope] Context. */ subscribeOnColumnChange: function (columnName, handler, scope) { this.on(this.columnChangeEventPrefix + columnName, handler, scope || this); }, /** * Performs an undefined event for changes to the attribute of the view model. * @protected * @param {String} columnName The name of the attribute of the view model. * @param {Function} handler An event handler for the modification of the presentation model attribute. * @param {Object} [scope] Context. */ unsubscribeOnColumnChange: function (columnName, handler, scope) { this.un(this.columnChangeEventPrefix + columnName, handler, scope || this); }, /** * Get the value of the field by name * For reference types, it returns a copy, it is necessary for correct work of tracking the change of model values * @param {String} key Field name * @return {String/Number/Date/Boolean/Object} Field Value */ get: function (key) { if (this.isTracking) { this.dependentProperties[key] = true; } return this.model.get(key); }, /** * Gets previous attribute value by key. * @param {String} key Attribute key. * @return {String|Number|Date|Boolean|Object} Attribute value. */ getPrevious: function (key) { return this.model.previous(key); }, /** * Returns value of lookup. * @throws {Terrasoft.exceptions.UnsupportedTypeException} * Throws exception UnsupportedTypeException if argument is not lookup and safe is false. * @param {String} columnName Name of lookup column. * @param {Boolean} safe Enable safe mode. * @return {String/Number/Date/Boolean/Object} Lookup value. */ getLookupValue: function (columnName, safe) { var column = this.columns[columnName]; if (column && (column.isLookup || safe)) { var columnValue = this.get(columnName); return columnValue && columnValue.value; } throw new Terrasoft.UnsupportedTypeException(); }, /** * Set a hash of attributes (one or many) on the model. If any of the attributes change the model's state, a * "change" event will be triggered on the model. Change events for specific attributes are also triggered, and * you can bind to those as well, for example: change:title, and change:content. * You may also pass individual keys and values. * @param {Mixed} key Name of the field or multiple attributes. * @param {String/Number/Date/Boolean/Object} value Value of the field. * @param {Object} options Options. */ set: function (key, value, options) { if (this.isTracking) { return; } this._defineAttributeProperty(key); this.model.set(key, value, options); }, /** * Trigger one or many events, firing all bound callbacks. * @param {String} event Event name. * @param {String/Number/Date/Boolean/Object} value Value of the field. */ trigger: function (event, value) { this.model.trigger(event, this, value); }, /** * Returns the model column by name. * @param {String} columnName Name of the column. * @return {Object} Column. */ getColumnByName: function (columnName) { return this.columns[columnName]; }, /** * Determines whether the model was changed on the client * @return {Boolean} Returns true if the model data has been changed */ getIsChanged: function () { var changedValues = this.changedValues = this.changedValues || {}; return !Terrasoft.isEmptyObject(changedValues); }, /** * Indicates if attribute was changed * @param attribute * @returns {boolean} */ isAttributeChanged: function (attribute) { return !Ext.isEmpty(this.changedValues && this.changedValues[attribute]); }, /** * Enables \ disables the dependency search mode. After power-on, the {@link #isTracking} state is set, * and all requested model attributes are written to an array {@link #dependentProperties} * @param {Boolean} isTracking You must pass true to enable search mode or false to disable */ setTrackingState: function (isTracking) { if (this.isTracking === isTracking) { return; } this.isTracking = isTracking; if (isTracking) { this.dependentProperties = {}; } }, /** * Returns collection of the model bindings. * @return {Terrasoft.Collection} Collection of the bindings. */ getBindMap: function () { return this.bindMap; }, /** * Inserts binding to the binding map collection. * @protected * @param {Object} binding Binding object. * @param {String} binding.attributeName Name of the model attribute to bind. * @param {Terrasoft.core.enums.BindingType} binding.type Type of the binding. * @param {String} binding.propertyName Property which were bound to the model attribute. */ addBindMap: function (binding) { if (!Ext.isObject(binding)) { throw new Terrasoft.UnsupportedTypeException(); } var attributeName = binding.attributeName; var propertyName = binding.propertyName; if (Ext.isEmpty(attributeName) || Ext.isEmpty(propertyName)) { return; } var bindMap = this.bindMap || (this.bindMap = new Terrasoft.Collection()); var existingBindMapItem = bindMap.find(attributeName); var bindMapItem = existingBindMapItem || { type: binding.type }; bindMapItem[propertyName] = attributeName; if (!existingBindMapItem) { bindMap.add(attributeName, bindMapItem); } }, /** * Returns model initial attribute value. * @param {String} name Attribute name. * @return {Object} */ getInitialValue: function (name) { var values = this.values || {}; var initialValue = values[name]; if (!initialValue) { var column = this.getColumnByName(name); if (column) { initialValue = column.value; } } return initialValue; }, /** * Check if model has attribute. * @param {String} attributeName Attribute name. * @return {Boolean} */ hasAttribute: function (attributeName) { return this.model.has(attributeName); }, /** * Returns the inverse value. Used for the converter in bindings to a value. * @param {boolean} value Value. * @return {boolean} Inverse value. */ invertBooleanValue: function (value) { return !value; }, /** * Converts value to boolean. * @param {Mixed} value Value. * @return {Boolean} Converted value. */ toBoolean: function (value) { return Boolean(value); }, /** * Base implementation of prepareList event handler. * @param {String} filter Filter. * @param {Terrasoft.Collection} list Drop-down List. */ onPrepareList: function (filter, list) { list.fireEvent("dataloaded", list); }, /** * Returns model attributes json string. */ getJSON: function () { return this.model.toJSON(); } });