/*
* Your installation or use of this SugarCRM file is subject to the applicable
* terms available at
* http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
* If you do not agree to all of the applicable terms or do not have the
* authority to bind the entity as an authorized representative, then do not
* install or use this SugarCRM file.
*
* Copyright (C) SugarCRM Inc. All rights reserved.
*/
const Acl = require('core/acl');
const Component = require('view/component');
const PluginManager = require('core/plugin-manager');
const Language = require('core/language');
const Template = require('view/template');
/**
* SugarField widget. A field widget is a low level field widget. Some examples
* of fields are text boxes, date pickers, drop down menus.
*
* ## Creating a SugarField
* SugarCRM allows for customized "fields" which are visual representations of a type of data (e.g. url would
* be displayed as a hyperlink).
*
* ### Anatomy of a SugarField
* Field files reside in the **`sugarcrm/clients/base/fields/{field_type}`** folder.
*
* Inside the {field_type} directory is a set of files that define templates for different views and field controller.
* A typical directory structure will look like the following:
* <pre>
* clients
* |- base
* |- bool
* |- bool.js
* |- detail.hbs
* |- edit.hbs
* |- list.hbs
* |- int
* ...
* |- text
* ...
* |- portal
* |- portal specific overrides
* |- mobile
* |- mobile specific overrides
* </pre>
* **`[sugarFieldType].js`** files are optional.
* Sometimes a SugarField needs to do more than just display a simple input element, other times input elements
* need additional data such as drop down menu choices. To support advanced functionality, just add your additional
* controller logic to **`[sugarFieldType].js`** javascript file where sugarFieldType is the type of the SugarField.
* Example for `bool.js` controller:
* ```
* const ViewManager = require('view/view-manager');
* ({
* events: {
* handler: function() {
* // Actions
* }
* },
*
* initialize: function(options) {
* ViewManager.Field.prototype.initialize(options);
* // Your constructor code here follows...
* },
*
* unformat: function(value) {
* value = this.el.children[0].children[1].checked ? '1' : '0';
* return value;
* },
*
* format: function(value) {
* value = (value == '1') ? true : false;
* return value;
* }
* })
* ```
*
* **`.hbs`** files contain your templates corresponding to the type of {@link View/View} the field is to be displayed on.
* Sugar uses Handlebars.js as its client side template of choice. At this time no other templating engines are
* supported. Sample:
* ```
* <span name="{{name}}">{{value}}</span>
* ```
*
* These files will be used by the metadata manager to generate metadata for your SugarFields and pass them onto the
* Sugar JavaScript client.
*
* @module View/Field
* @class
* @extends View/Component
*/
const Field = Component.extend({
/**
* HTML tag of the field.
* @type {string}
* @memberOf View/Field
* @instance
*/
fieldTag: "input",
/**
* Initializes this view.
*
* @param {Object} options The `Backbone.View` initialization options.
* @memberOf View/Field
* @instance
*/
initialize: function(options) {
PluginManager.attach(this, 'field');
Component.prototype.initialize.call(this, options);
/**
* ID of the field (autogenerated).
* @type {number}
* @name sfId
* @memberOf View/Field
* @instance
*/
this.sfId = options.sfId;
/**
* Reference to the view this field is attached to.
* @type {View/View}
* @name view
* @memberOf View/Field
* @instance
*/
this.view = options.view;
/**
* View definitions for the field (doesn't include vardefs).
*
* The view definitions are the properties used for field rendering.
* Example: A `link` property can specify if the field should be
* displayed as a link.
*
* @type {Object}
* @name viewDefs
* @property {Object} defs A hash of properties that
* will override the field definitions (vardefs).
* @memberOf View/Field
* @instance
*/
this.viewDefs = options.viewDefs || {};
/**
* Field name.
* @type {string}
* @name name
* @memberOf View/Field
* @instance
*/
this.name = this.viewDefs.name || options.def && options.def.name;
var fieldDefs = this.model && this.model.fields ? this.model.fields[this.name] : null;
/**
* Field definitions (vardefs).
*
* Field definitions are properties that define the field and its
* internal behavior.
*
* @type {Object}
* @name fieldDefs
* @memberOf View/Field
* @instance
*/
this.fieldDefs = _.extend({}, fieldDefs, this.viewDefs.defs);
/**
* Field metadata definition (fieldDefs + viewdefs).
*
* Viewdef are copied over vardef.
* @type {Object}
* @name def
* @memberOf View/Field
* @deprecated
* @instance
*/
this.def = _.extend({}, fieldDefs, options.def);
/**
* Widget type (text, bool, int, etc.).
* @type {string}
* @name type
* @memberOf View/Field
* @instance
*/
this.type = this.viewDefs.type || this.def.type || this.fieldDefs.type;
// TODO deprecate this fallback, since label vs vname is already patched in metadata
var oldLabel = this.def.label || this.def.vname || this.name;
/**
* i18n-ed field label.
* @type {string}
* @name label
* @memberOf View/Field
* @instance
*/
this.label = Language.get(this.viewDefs.label || oldLabel, this.module);
/**
* Compiled template.
* @type {Function}
* @name template
* @memberOf View/Field
* @instance
*/
this.template = Template.empty;
// Bind validation error event
// Note we bind it regardless of which view we on (only need for edit type views)
if (this.model) {
this.model.on('error:validation:' + this.name, this.handleValidationError, this);
this.model.on('validation:start attributes:revert', this.removeValidationErrors, this);
this.model.on('acl:change:' + this.name, this.handleAclChange, this);
}
},
/**
* Defines fallback rules for ACL checking.
*
* For example, if a user doesn't have `edit` permission for the given field
* the template falls back to `detail` view template.
*
* @type {Object}
* @memberOf View/Field
*/
viewFallbackMap: {
'edit': 'detail'
},
/**
* Handler for when an ACL changes at the field-level.
*
* @memberOf View/Field
* @instance
*/
handleAclChange: function() {
/* FIXME SC-3363: The previous action needs to be cleared in order to
load the correct template when the user potentially loses access to a
field. SC-3363 will address this. */
this._resetAction();
this.render();
},
/**
* Sets this field's action to undefined.
*
* @memberOf View/Field
* @instance
* @protected
*/
_resetAction: function () {
this.action = void 0;
},
/**
* Checks ACLs to see if the current user has access to action.
*
* @param {string} action Action name.
* @return {boolean} Flag indicating if the current user has access to the given action.
* @see {@link View/Field#_loadTemplate}
* @memberOf View/Field
* @instance
* @private
*/
_checkAccessToAction: function(action) {
return Acl.hasAccessToModel(action, this.model, this.name);
},
/**
* Gets the fallback template for this field's view.
*
* @param {string} viewName Name of the view.
* @return {string} If disabled and `viewName` is 'disabled', then 'edit'.
* If not, then either `this.view.fallbackFieldTemplate` or
* 'detail'.
* @protected
* @memberOf View/Field
* @instance
*/
_getFallbackTemplate: function(viewName) {
return (this.isDisabled() && viewName === 'disabled') ? 'edit' :
(this.view.fallbackFieldTemplate || 'detail');
},
/**
* Loads the template for this field.
*
* @memberOf View/Field
* @instance
* @private
*/
_loadTemplate: function() {
var fallbackFieldTemplate;
// options.viewName or view metadata type is used to override the template
var viewName = this.options.viewName || this.options.def.view ||
(this.view.meta && this.view.meta.type ? this.view.meta.type : this.view.name);
var actionName = this.action;
if (this.isDisabled() && viewName === 'edit') {
viewName = this.action;
} else {
actionName = this.action || (this.view.action && this.view.name != this.view.action ? this.view.action : viewName);
}
while (viewName) {
if (this._checkAccessToAction(actionName)) break;
viewName = this.viewFallbackMap[viewName];
actionName = viewName;
}
if (viewName) {
// Set fallback template to base/view or default.
var moduleName = this.module || this.context.get('module');
fallbackFieldTemplate = this._getFallbackTemplate(viewName);
this.template = Template.getField(this.type, viewName, moduleName, fallbackFieldTemplate) ||
// Fallback to text field if template is not defined for this type
Template.getField('base', viewName, moduleName, fallbackFieldTemplate) ||
// Safeguard with an empty template
Template.empty;
} else {
// Safeguard with an empty template
this.template = Template.empty;
}
// Update template name and action.
// These properties are useful for a client app to make decisions when formatting values, rendering, etc.
/**
* Template (view) name.
*
* The view name can be different from the one the field belongs to.
* The template is selected based on ACLs. It may also be overridden by field's metadata definition.
* @type {string}
* @memberOf View/Field
* @name tplName
* @instance
*/
this.tplName = viewName;
/**
* Action name.
*
* The action the field is rendered for. Usually, the action name
* equals to {@link View/Field#tplName}.
*
* @type {string}
* @memberOf View/Field
* @name action
* @instance
*/
this.action = actionName;
},
/**
* Overrides default Backbone.Events to also use custom handlers.
*
* The events hash is similar to Backbone.View events hash.
* You can specify event handler as method of field or as name of event
* that should be triggered. You cannot use javascript code in metadata.
* The framework stores the event handlers as anonymous function bound
* to this field as part of the field instance with the `"callback_"`
* prefix. When event is fired it calls `triggerMetadataEvent` method
* if it exists in field. The default behavior of `triggerMetadataEvent`
* is implemented in {@link MetadataEventDriven} plugin.
*
* ```
* events: {
* handler: triggerSomeEvent;
* }
* ```
*
* triggerSomeEvent should be method of certain field.
*
* OR
*
* ```
* events: {
* handler: 'fire:some:event';
* }
* ```
*
* @param {Object} events Hash of events and their handlers.
* @private
*/
delegateEvents: function(events) {
// We may have:
// this.events -- comes from custom .js controllers
// this.def.events -- comes from metadata. See, for example, buttons section in portal.js file
// FIXME SC-5676: The metadata events should typically be extended
// or mixed-in to the events, not OR'ed with `this.events`. SC-5676
// should address refactoring this method.
events = events || _.result(this, 'events') || (this.def ? this.def.events : null);
if (!events) return;
events = _.clone(events);
_.each(events, function(eventHandler, handlerName) {
var callback = this[eventHandler];
// If our callbacks / events have not been registered in field,
// go ahead and registered it as method.
if (!callback && _.isString(eventHandler)) {
callback = _.bind(function(event) {
if (_.isFunction(this.triggerMetadataEvent)) {
this.triggerMetadataEvent(event, eventHandler);
}
}, this);
this['callback_' + handlerName] = callback;
events[handlerName] = 'callback_' + handlerName;
}
if (!_.isFunction(eventHandler) && !callback) {
delete events[handlerName];
}
}, this);
Backbone.View.prototype.delegateEvents.call(this, events);
},
/**
* Renders a field widget.
*
* This method checks ACLs to choose the correct template.
* Once the template is rendered, DOM changes are bound to the model.
*
* @return {View/Field} The instance of this field.
* @memberOf View/Field
* @instance
* @protected
*/
_render: function() {
this._loadTemplate();
if (this.model instanceof Backbone.Model) {
/**
* Model property value.
* @type {string}
* @name value
* @memberOf View/Field
* @instance
*/
this.value = this.getFormattedValue();
}
/**
* Override this property with a specific direction string if the
* field has a set direction that it always follows.
*
* Override this property with a function if logic is needed to
* determine the direction of the field. The function should return
* either a `string` indicating the direction of the field or
* `undefined`. This function is only called after the value is set
* so that we can base the direction on the value.
*
* The default value undefined means that the field would use the
* inherited direction from the DOM.
*
* Example using a function:
*
* ```
* direction: function() {
* return this.isRTL ? 'rtl' : 'ltr';
* }
* ```
* @type {Function|string|undefined}
* @name direction
* @memberOf View/Field
* @instance
*/
/**
* The direction of the field. The default value `undefined` means
* that the field would use the inherited direction from the DOM.
*
* Do not override this property directly; override
* {@link View/Field#direction} instead.
*
* @type {string|undefined}
* @name dir
* @memberOf View/Field
* @instance
* @readonly
*/
this.dir = _.result(this, 'direction');
if (Language.direction === this.dir) {
delete this.dir;
}
this.unbindDom();
this.$el.html(this.template(this) || '');
// Adds classes to the component based on the metadata.
if(this.def && this.def.css_class) {
this.getFieldElement().addClass(this.def.css_class);
}
this.$(this.fieldTag).attr('dir', this.dir);
this.bindDomChange();
return this;
},
/**
* Gets the corresponding field DOM element.
*
* This method will return the placeholder element.
* Override this method in the subclass to point the specified field
* element.
*
* @return {Object} DOM Element
* @memberOf View/Field
* @instance
*/
getFieldElement: function() {
return this.$el;
},
/**
* Binds DOM changes to a model.
*
* The default implementation of this method binds value changes of
* {@link View/Field#fieldTag} element to model's `Backbone.Model#set`
* method. Override this method if you need custom binding.
*
* @memberOf View/Field
* @instance
*/
bindDomChange: function() {
if (!(this.model instanceof Backbone.Model)) return;
var self = this;
var el = this.$el.find(this.fieldTag);
el.on("change", function() {
self.model.set(self.name, self.unformat(el.val()));
});
},
/**
* Binds model changes to this field.
*
* The default implementation makes sure this field gets re-rendered
* whenever the corresponding model attribute changes.
*
* @memberOf View/Field
* @instance
*/
bindDataChange: function() {
if (this.model) {
this.model.on("change:" + this.name, function(model, value) {
// FIXME: we need to track the `options` send by
// `model.set()` and only call `this.render()` if it didn't
// came from `bindDomChange`
if (this.action === 'edit') {
// Should directly set the value in edit instead of re-rendering the whole field (see MAR-1617).
this.$(this.fieldTag).val(this.format(value));
} else {
this.render();
}
}, this);
}
},
/**
* Checks to see if the field's value has been changed from the saved model
* This is not the same as {@link Backbone.Model#hasChanged} which checks
* if the model has changed from the last time it was set whereas this
* function checks if what is currently in the input field is the same as
* what is synced on the model
*
* @return {boolean} `true` if the value is different, `false` if field is
* synced with the saved model
* @memberOf View/Field
* @instance
*/
hasChanged: function() {
return !_.isEqual(this.format(this.model.getSynced(this.name)), this.format(this.model.get(this.name)));
},
/**
* Formats the value to be used in handlebars template and displayed on
* screen.
*
* The default implementation returns `value` without modifying it.
* Override this method to provide custom formatting in field
* controller (`[type].js` file).
*
* @param {Array|Object|string|number|boolean} value The value to format.
* @return {Array|Object|string|number|boolean} Formatted value.
* @memberOf View/Field
* @instance
*/
format: function(value) {
return value;
},
/**
* Unformats the value for storing in a model. This should do the
* inverse of {@link #format}.
*
* The default implementation returns `value` without modifying it.
* Override this method to provide custom unformatting in field
* controller (`[type].js` file).
*
* @param {string} value The value to unformat.
* @return {Array|Object|string|number|boolean} Unformatted value.
* @memberOf View/Field
* @instance
*/
unformat: function(value) {
return value;
},
/**
* Returns the value of this field in the associated model.
*
* If you need to override the formatted value please override
* {@link View/Field#format}.
*
* @return {Array|Object|string|number|boolean} The formatted data as
* provided by {@link View/Field#format}.
* @memberOf View/Field
* @instance
*/
getFormattedValue: function() {
return this.format(this.model.has(this.name) ? this.model.get(this.name) : null);
},
/**
* Handles validation errors.
*
* The default implementation does nothing.
* Override this method to provide custom display logic.
* ```
* ViewManager.Field = ViewManager.Field.extend({
* handleValidationError: function(errors) {
* // Your custom logic goes here
* }
* });
* ```
*
* @param {Object} errors hash of validation errors
* @memberOf View/Field
* @instance
*/
handleValidationError: function(errors) {
// Override this method
},
/**
* Removes the validation error properties on the field that were set by
* {@link #handleValidationError}.
*
* The default implementation does nothing.
*
* Override this method to provide custom logic:
*
* ```
* ViewManager.Field = ViewManager.Field.extend({
* removeValidationErrors: function(errors) {
* // Your custom logic goes here
* }
* });
* ```
*
* @memberOf View/Field
* @instance
*/
removeValidationErrors: function() {
// Override this method
},
/**
* Gets the HTML placeholder for a field.
*
* @return {Handlebars.SafeString} HTML placeholder for the field as
* Handlebars safe string.
* @memberOf View/Field
* @instance
*/
getPlaceholder: function() {
return new Handlebars.SafeString('<span sfuuid="' + this.sfId + '"></span>');
},
/**
* Disables edit mode by switching the element as detail mode.
*
* @param {boolean} disable `true` or `undefined` to disable the edit mode
* otherwise, it will restore back to the previous mode.
* @param {Object} [options = {}] A hash of options.
* @param {boolean} [options.trigger] `true` to trigger the `field:disabled`
* event in the context. The event passes 2 arguments: the field name and
* the `disable` boolean.
* @memberOf View/Field
* @instance
*/
setDisabled: function(disable, options = {}) {
disable = _.isUndefined(disable) ? true : disable;
let swapped = false;
if(disable && this.isDisabled() === false) {
//Set disabled
this._previousAction = this.action;
this.action = 'disabled';
this.render();
swapped = true;
} else if(disable === false && this.isDisabled()) {
//disabled release
this.action = this._previousAction;
delete this._previousAction;
this.render();
swapped = true;
}
if (swapped && options.trigger) {
this.context.trigger('field:disabled', this.name, disable, this);
}
},
/**
* Checks if this field is disabled.
*
* @return {boolean} `true` if this field is disabled, `false` otherwise.
* @memberOf View/Field
* @instance
*/
isDisabled: function() {
return (this.action === 'disabled');
},
/**
* Set view name of this field.
* This only switches the template reference.
*
* @param {string} view View name.
* @memberOf View/Field
* @instance
*/
setViewName: function(view) {
this.options.viewName = view;
},
/**
* Set action name of this field.
* This switches action name as well as the template reference.
*
* @param {string} name Action name.
* @memberOf View/Field
* @instance
*/
setMode: function(name) {
if(this.isDisabled()) {
this._previousAction = name;
} else {
this.action = name;
}
this.setViewName(name);
this.render();
},
/**
* Unbinds DOM changes from this field's element.
*
* This method performs the opposite of what
* {@link View/Field#bindDomChange} method does.
* Override this method if you need custom logic.
*
* @memberOf View/Field
* @instance
*/
unbindDom: function() {
this.$el.find(this.fieldTag).off();
},
/**
* Disposes a field.
*
* Calls {@link View/Field#unbindDom} and {@link View/Component#_dispose}
* method of the base class.
*
* @protected
* @memberOf View/Field
* @instance
*/
_dispose: function() {
PluginManager.detach(this, 'field');
this.unbindDom();
Component.prototype._dispose.call(this);
},
/**
* Gets a string representation of this field.
*
* @return {string} String representation of this field.
* @memberOf View/Field
* @instance
*/
toString: function() {
return "field-" + this.name + "-" + this.sfId + "-" +
Component.prototype.toString.call(this);
},
/**
* @inheritdoc
* @memberOf View/Field
*/
_show: function() {
this.getFieldElement().removeClass('hide').show();
this.updateVisibleState(true);
},
/**
* @inheritdoc
* @memberOf View/Field
*/
_hide: function() {
this.getFieldElement().addClass('hide').hide();
this.updateVisibleState(false);
},
/**
* Compares formatted values for equality.
*
* @param {View/Field} other Other field component.
* @return {boolean} It returns `true` for equal value.
* @memberOf View/Field
* @instance
*/
equals: function(other) {
return this.type === other.type && _.isEqual(this.getFormattedValue(), other.getFormattedValue());
},
// FIXME: THIS METHOD NEEDS TO BE DOCUMENTED!
closestComponent: function(name) {
if (!this.view) {
return;
}
if (this.view.name === name) {
return this.view;
}
return this.view.closestComponent(name);
}
});
module.exports = Field;