Ext.ns("Terrasoft");
/**
* Abstract base class of a control.
* Implements logic for creating, initializing, rendering and destroying for visual controls.
* @private
* @abstract
*/
Ext.define("Terrasoft.controls.Component", {
extend: "Terrasoft.core.BaseObject",
alternateClassName: "Terrasoft.Component",
mixins: {
bindable: "Terrasoft.Bindable",
expandableTip: "Terrasoft.ExpandableTip"
},
statics: {
/**
* Unique identifier generator.
* @private
* @static
* @type {Ext.data.SequentialIdGenerator}
*/
idGenerator: null,
/**
* Default conponent identifier suffix.
* @private
* @static
* @type {String}
*/
defaultComponentId: "t-comp",
/**
* The method returns a serial identifier generator.
* @private
* @static
* @return {Ext.data.SequentialIdGenerator}
*/
getIdGenerator: function () {
if (this.idGenerator === null) {
this.idGenerator = new Ext.data.SequentialIdGenerator({
prefix: this.defaultComponentId,
seed: 0
});
}
return this.idGenerator;
},
/**
* Generates unique identifier.
* @private
* @static
* @return {String}
*/
generateId: function () {
var idGenerator = this.getIdGenerator();
return idGenerator.generate();
}
},
id: null,
/**
* Focus index
* @type {Number}
*/
tabIndex: 1,
/**
* A reference to the wrapper element of the component.
* @private
* @type {Ext.dom.Element}
*/
wrapEl: null,
/**
* Regular expression pattern for parsing HTML component markup for parameterizing inline styles
* @private
* @type {String}
*/
/*jshint quotmark:false */
//jscs: disable
styleRegexTemplate: 'style\\s*=\\s*"{(\\w*)}"',
//jscs: enable
/*jshint quotmark:double */
/**
* Regular expression pattern for parsing HTML component markup for parametrizing CSS classes
* @private
* @type {String}
*/
/*jshint quotmark:false */
//jscs: disable
cssRegexTemplate: 'class\\s*=\\s*"{(\\w*)}"',
//jscs: enable
/*jshint quotmark:double */
/**
* A flag that indicates that the component is rendered.
* @readonly
* @type {Boolean}
*/
rendered: false,
/**
* A flag that indicates that the component is in the rendering phase.
* @readonly
* @type {Boolean}
*/
rendering: false,
uses: ["Ext.Element", "Ext.DomHelper", "Ext.XTemplate", "Ext.ComponentQuery", "Ext.ComponentLoader", "Ext.EventManager"],
/**
* This property provides a shorter alternative to creating objects than using a full class name.
* Using `className` is the most convenient way to define components, especially in containers.
* For example, the definition of a container with three text elements in the general form is written as follows:
*
* var container = Ext.create('Terrasoft.Container', {
* id: 'someContainer',
* items: [
* Ext.create('Terrasoft.textedit.TextEdit', {
* id: 'textEdit-1'
* }),
* Ext.create('Terrasoft.textedit.TextEdit', {
* id: 'textEdit-2'
* }),
* Ext.create('Terrasoft.textedit.TextEdit', {
* id: 'textEdit-3'
* })
* ]
* });
*
* Using className, the code can be rewritten as follows:
*
* var container = Ext.create('Terrasoft.Container', {
* id: 'someContainer',
* items: [
* {
* className: 'Terrasoft.TextEdit',
* id: 'textEdit-1'
* },
* {
* className: 'Terrasoft.TextEdit',
* id: 'textEdit-2'
* },
* {
* className: 'Terrasoft.TextEdit',
* id: 'textEdit-3'
* }
* ]
* });
*
* When creating your component successor, you must specify a unique className.
* @type {String}
*/
/**
* A template string that performs the basic rendering function.
* @protected
* @type {String}
*/
renderTpl: "{%this.renderComponent(out, values)%}",
/**
* Class to disable the control.
* @protected
* @property {String} disabledClass
*/
disabledClass: "",
/**
* An object that describes the {@link Ext.DomQuery DomQuery-selectors} for the internal DOM elements of the
* component. Selectors must be specified by the component's successors during rendering.
* The best place to create selectors are the {@link #getTpl getTpl ()} and {@link #getTplData getTplData ()}
* methods. If there are no selectors at the time of the {@link #afterrender afterrender} event, an exception will
* be thrown. When the component is rendered in several nested containers, the component itself does not actually
* place its HTML markup in the DOM. It generates it and passes it to its container and subscribes to the
* container's afterrender event, in whose handler it starts its afterrender event. Thus, there can be an
* indefinite time between the insertion time of the HTML component markup in the DOM and the time of the
* afterrender event. The component needs to be able to find itself in the DOM.
* For this, the selector mechanism is implemented, which ensures that the component has at least two elements:
* {@link #wrapEl wrapEl} and {@link #el el}.
* The selector mechanism runs on the {@link #afterrender afterrender} event when HTML is already
* inserted in the DOM. Upon completion of the mechanism, the name of the selector will be the name of the
* component's property, and the value will become a reference to the DOM element.
* @protected
* @type {Object}
*/
selectors: null,
/**
* {@link Ext.XTemplate Template} component HTML markup.
* When rendering the control, the {@link #getTpl getTpl ()} method must return the element template.
* But if the control in any of its states has the same HTMl-markup,
* then there is no need to implement a method for generating a template, just redefine the tpl property and
* the base implementation of the {@link #getTpl getTpl ()} method will return this template.
* @type {String []}
*/
tpl: null,
/**
* An object containing parameters for the {@link #tpl template} of the control.
* When rendering the control, the {@link #getTplData getTplData ()} method must return the parameter object for
* the template. If there is no need to implement a method for generating parameters when the control
* is not dynamic, it is enough to override the tplData property and the base implementation of
* the {@link #getTplData getTplData ()} method will return the object.
* @type {Object}
*/
tplData: null,
/**
* @cfg {Ext.dom.Element} renderTo
* Specifies a reference to the {@link Ext.Element Ext.Element} into which the control will be rendered.
* If the property is specified, then the component will start rendering immediately after the initialization
* is complete. If the property is not specified, then you must call the {@link #render render ()} method yourself
* @type {Object}
*/
renderTo: null,
/**
* The object for specifying the inline styles of the component specified in the template.
* If the {@link #tpl template} specifies the name of the style, then the style object must have a property with
* the same name. If a property with styles is not found, the attribute with styles will be removed
* from the template. You can specify the component styles in either of the two methods {@link #getTpl getTpl ()}
* and {@link #getTplData getTplData ()}.
* Example:
*
* tpl: [
* '<div id="someId" style="{wrapStyle}">',
* '<input type="text" style="{elStyle}">',
* '</div>'
* ],
* styles: {
* wrapStyle: {
* border: '1px solid black;',
* 'background-color': 'white',
* padding: '5px'
* }
* }
*
* As a result, following HTML markup will be inserted into the DOM:
*
* '<div id="someId" style="border:1px solid black; background-color:white; padding:5px;">',
* '<input type="text" >',
* '</div>'
*
* @type {Object}
*/
styles: null,
/**
* The object for specifying the CSS classes of the component specified in the template.
* If the {@link #tpl template} specifies the class name, then the class object must have a property with the
* same name. If a property with a class is not found, the attribute with the class will be removed from the
* template. You can specify component classes in either of the two methods {@link #getTpl getTpl ()}
* and {@link #getTplData getTplData ()}.
* Example:
*
* tpl: [
* '<div id="someId" class="{wrapClass}">',
* '<input type="text" class="{elClass}">',
* '</div>'
* ],
* classes: {
* wrapClass: [
* 'component-wrap',
* 'component-disabled',
* 'component-firstitem.component-important'
* ]
* }
*
* As a result, such HTML markup will be inserted into the DOM:
*
* '<div id="someId" class="component-wrap component-disabled component-firstitem.component-important">',
* '<input type="text" >',
* '</div>'
*
* @type {Object}
*/
classes: null,
/**
* The flag indicates the visibility of the control.
* @type {Boolean}
*/
visible: true,
/**
* The flag indicates that the component is enabled
* @type {Boolean}
*/
enabled: true,
/**
* A string of finished HTML markup for insertion into the DOM.
* At the moment of rendering of the control if the finished HTML-marking is given, then the template construction
* mechanism does not start And the specified line is inserted into the DOM. Later, on
* the {@link #afterrender, the aftertender} event the selector mechanism is started.
* (see {@link #selectors selectors})
* @type {String}
*/
html: null,
/**
* The string of additional parameters for the component. Is not used in the code.
* The property is intended for use by the developer.
* @type {String}
*/
tag: "",
/**
* An attribute that indicates that this object is a component.
* @type {String}
*/
isComponent: true,
/**
* The value of the marker DOM attribute data-item-marker.
* @type {String}
*/
markerValue: "",
/**
* The sign of the mask display.
* @type {Boolean}
*/
maskVisible: false,
/**
* The mask identifier.
* @type {String}
* @private
*/
maskId: "",
/**
* Mask delay time
* @type {Number}
* @private
*/
maskTimeout: null,
/**
* Parent element.
* @type {Terrasoft.Component}
* @private
*/
ownerCt: null,
/**
* Event handlers for the parent component. The object supports the implementation of the 'destroy' method
* see {@link Ext.util.Observable # addListener addListener} with the 'destroyable' parameter.
* @type {Object}
* @private
*/
ownerCtListeners: null,
/**
* Dom element attributes.
* @type {Object}
* @private
*/
domAttributes: null,
/**
* Owner component view items property name.
* @type {String}
* @private
*/
ownerCtPropertyName: null,
/**
* True if need observe component mutations.
*/
observeMutations: null,
/**
* Config for MutationObserver.
*/
mutationConfig: null,
/**
* The direction of the text. Left to right or right to left.
* @protected
* @type {String}
*/
direction: null,
/**
* Class constructor. Performs component initialization, id generation, if not specified, and registration
* in {@link Ext.ComponentManager Component Manager}.If the {@link #renderTo renderTo} parameter is specified
* in the configuration object, the constructor calls the {@link #render render ()} method when the renderTo
* parameter is passed as an argument.
* @param {Object} config configuration object initializing the instance of the component.
*/
constructor: function (config) {
this.className = this.$className;
this.mixins.expandableTip.constructor.call(this, config);
this.mixins.bindable.constructor.call(this, config);
this.callParent(arguments);
this.addEvents(
/**
* @event init
* Triggers when component initialization is complete. If the developer in the heir class overrides the
* constructor, the event will start when the component's base constructor completes.
* Therefore, initialization is best implemented in the {@link #init init ()} method
*/
"init",
/**
* @event added
* Triggers after the component has been added to the container.
* It is assumed that the container after adding the element will call the {@link #added added ()} method.
* The base container element implements this logic, but if the user decides to write his own implementation
* of the container, then he must call this method himself.
* @param {Terrasoft.Component} component component
* @param {Terrasoft.Container} ownertCt container to which the component was added
*/
"added",
/*'move',
'removed',*/
"enabledchanged", "visiblechanged",
/**
* @event beforererender
* Works before the component has been rendered and its HTML representation is in the DOM.
* @param {Terrasoft.Component} component component
*/
"beforererender",
/**
* @event afterrender
* Triggers after the component has been rendered and its HTML representation is in the DOM.
* @param {Terrasoft.Component} component component
* @param {Terrasoft.Container} ownertCt container in which the component was rendered
*/
"afterrender",
/**
* @event afterrerender
* Triggers after the component completes the rendering and its HTML representation is updated in the DOM.
* @param {Terrasoft.Component} component component
* @param {Terrasoft.Container} ownertCt container in which the component is rendered
*/
"afterrerender",
/**
* @event destroy
* Works before the final destruction of the control. Runs in the implementation of the base destructor,
* before removing it from the component manager, deleting all event handlers, and destroying the
* component's DOM if it was rendered. This is the last opportunity to clean the heirs before the final
* destruction of the component.
* @param {Terrasoft.Component} component component
*/
"destroy",
/**
* Fires during the start of the run check method.
* @event canExecute
* @param {Object} config
* @param {Function} config.method Resume function.
* @param {Array} config.args Resume function arguments.
* @param {Object} config.scope Resume function scope.
* @return {Boolean} True if allowed to continue execute check method otherwise false.
*/
"canExecute",
/**
* Fires when DOM element mutate.
*/
"mutate"
//'resize',
//'focus',
//'blur'
);
if (Ext.isEmpty(this.id)) {
this.id = Terrasoft.Component.generateId();
}
this.init();
this.fireEvent("init", this);
Terrasoft.ComponentManager.register(this);
if (this.renderTo) {
this.render(this.renderTo, null);
}
},
/**
* Inits mutatio observer.
* @private
*/
initMutationObserver: function () {
var target = document.getElementById(this.id);
this.mutationObserver = new MutationObserver(function (mutations) {
Terrasoft.each(mutations, function (mutation) {
this.fireEvent("mutate", mutation);
}, this);
}.bind(this));
this.mutationObserver.observe(target, this.mutationConfig);
},
/**
* Returns the parent element in the hierarchy of the controls.
* The method is used by the {@link Ext.util.Observable Observable} mixin to implement pop-up events.
* See {@link Ext.util.Observable # enableBubble enableBubble ()}
* @override
* @return {Terrasoft.Component} Parent element
*/
getBubbleParent: function () {
return this.ownerCt;
},
/**
* The method implements the mechanism {@link #selectors of selectors}. It starts when the component is rendered.
* Searches for the DOM elements of the component at the specified {@link # selectors} selectors and stores the
* references to the found elements in the instance of the class. If no wrapEl selectors are specified or the
* result of searching by the wrapEl selector will be null - an exception will be thrown.
* @private
* @throws {Terrasoft.UnknownException}
* Throws if the component was not rendered.
* @throws {Terrasoft.NullOrEmptyException}
* An exception throws if the component does not pass {@link #verifySelectors check selectors}.
* @throws {Terrasoft.ItemNotFoundException}
* An exception throws if the wrapEl selector does not find any html-element.
* @param {Ext.dom.Element} rootNode
* The root element of which will be searched. If an element is not specified, the search will be relative to the
* body of the document.
*/
applySelectors: function (rootNode) {
if (!this.rendered) {
throw new Terrasoft.UnknownException({
message: Terrasoft.Resources.Controls.Component.ComponentIsNotRendered
});
}
if (!this.verifySelectors()) {
throw new Terrasoft.NullOrEmptyException({
message: Terrasoft.Resources.Controls.Component.RequiredSelectorsAreNotDefined
});
}
var selectors = this.selectors;
var wrapElSelector = selectors.wrapEl;
var root = Ext.getDom(rootNode);
if (!root) {
root = Ext.getBody().dom;
}
var wrapEl = Ext.DomQuery.is(root, wrapElSelector) ? Ext.get(root) : this.selectNode(wrapElSelector, root);
if (wrapEl == null) {
throw new Terrasoft.ItemNotFoundException();
}
this.wrapEl = wrapEl;
var wrapDom = wrapEl.dom;
for (var selectorName in selectors) {
if (!selectors.hasOwnProperty(selectorName) || selectorName === "wrapEl") {
continue;
}
var selectorConfig = selectors[selectorName];
var selector;
if (Ext.isObject(selectorConfig)) {
selector = selectorConfig.selector;
} else {
selector = selectorConfig;
}
var selectedNode = this.selectNode(selector, wrapDom);
this[selectorName] = selectedNode;
}
this.bindSelectors();
},
/**
* The method looks for {@link Ext.Element} in the DOM at the specified {@link Ext.DomQuery DomQuery-selector}
* relative to the root element. If at least one of the parameters is not specified, the method throws an exception.
* @param {String} selector {@link Ext.DomQuery DomQuery-selector}
* @param {Ext.dom.Element} root The element to be searched for
* @return {Ext.dom.Element}
* @private
*/
selectNode: function (selector, root) {
if (!selector || !root) {
throw new Terrasoft.ArgumentNullOrEmptyException();
}
var selectNode = Ext.DomQuery.selectNode(selector, root);
// Hack. The selector by ID does not check the root
if (!selectNode && Ext.DomQuery.is(root, selector)) {
selectNode = root;
}
return Ext.get(selectNode);
},
/**
* The method prepares the rendering configuration object of the control.
* @return {Object}
* @private
*/
generateRenderConfig: function () {
var config = {};
var tpl = config.tpl = this.generateRenderTemplate();
if (!Ext.isEmpty(tpl)) {
config.tplData = {};
this.initRenderData(config.tplData);
}
return config;
},
/**
* The method generates a template for rendering the component. Not to be confused with the tpl template.
* @return {Ext.XTemplate}
* @private
*/
generateRenderTemplate: function () {
var tpl = Ext.XTemplate.getTpl(this, "renderTpl");
this.initRenderTemplate(tpl);
return tpl;
},
/**
* The main method of rendering the component. If the parameter {@link #html html} was specified in the
* configuration object of the component, the specified HTML markup is inserted into the DOM.
* Otherwise, the (see {@link #getTpl, getTpl ()}) parametrized HTML markup template is collected,
* the (see {@link #getTplData, getTplData ()}) parameters are prepared,
* the (see {@ link #processTemplate, processTemplate ()}) template preprocessing is performed.
* After that the template is compiled into HTML-code and added to the transferred buffer.
* Since the method is executed in the context of {@link Ext.XTemplate Ext.XTemplate}, then to get the reference
* to the component, you must use renderData.self.
* @param {String []} buffer Buffer for generating HTML
* @param {Object} renderData Parameters passed to the {@link #renderTpl renderTpl} template.
* @private
*/
renderComponent: function (buffer, renderData) {
var self = renderData.self;
if (self.html) {
Ext.DomHelper.generateMarkup(self.html, buffer);
return;
}
var tpl = self.getTpl();
var tplData = self.getTplData();
tpl = self.processTemplate(tpl, tplData);
var template = new Ext.XTemplate(tpl);
self.prepareTpl(template, tplData);
template.applyOut(tplData, buffer);
},
/**
* The method copies properties of the object with the name passed as a parameter (sourceName) from the component
* to the template parameter object passed as the (tplData) parameter. Used when preprocessing a template.
* @param {String} sourceName The name of the object with properties in the component
* @param {Object} tplData Parameter object for the rendering template
* @private
*/
copySourceToTplData: function (sourceName, tplData) {
var source = this[sourceName] || {};
var customClasess;
if (sourceName === "classes") {
customClasess = this.classes || {};
}
for (var propertyName in source) {
if (!source.hasOwnProperty(propertyName)) {
continue;
}
var sourceValue = Terrasoft.deepClone(source[propertyName]);
if (sourceName === "classes") {
sourceValue = Ext.Array.merge(tplData[propertyName] || [], customClasess[propertyName] || []);
} else if (tplData[propertyName]) {
continue;
}
tplData[propertyName] = Ext.isArray(sourceValue) ? sourceValue : Ext.apply({}, sourceValue);
}
},
/**
* The method searches for the regular expression regexTemplate in the tpl line.
* If a match is found, it performs the substitution according to the following algorithm:
*
* 1. If in the matching substring the value of the group is empty - the entire matched string is replaced by
* an empty string.
* 2. If there is no property in the rendering template parameter object with the name of the matched group,
* the entire matched string is replaced by an empty string.
* 3. The replaced replaceFn function is being executed.
*
* The method is used when preprocessing a rendering template, for parameterizing {@link #styles inline-styles}
* and {@link #classes of CSS classes}.
* @param {String} regexTemplate - A string for generating a regular expression for searching.
* It is expected that there will be at least one group in the regular expression.
* @param {String} tpl - The string in which the search and replace are performed.
* @param {Object} tplData - Parameter object of the rendering template
* @param {Function} replaceFn - Replace function
* @return {String} The converted template string
* @private
*/
processTpl: function (regexTemplate, tpl, tplData, replaceFn) {
var stylesRegex = new RegExp(regexTemplate, "gm");
return tpl.replace(stylesRegex, function (matchedString, groupValue) {
if (!groupValue) {
return "";
}
var group = tplData[groupValue];
if (!group) {
return "";
}
return replaceFn(group);
});
},
/**
* The method deletes all {@link Ext.Element Ext.Elements}, the references to which were set by
* the {@Link #selectors selectors} mechanism.
* In the case when the component does not have a reference to the element that is specified in the selector
* object, an exception is thrown.
* @private
* @param {Boolean} [removeOnlySelectors = false] (optional) specifies whether to remove both selectors and
* DOM elements
*/
removeElementsBySelectors: function (removeOnlySelectors) {
var selectors = this.selectors;
for (var selectorName in selectors) {
if (!selectors.hasOwnProperty(selectorName)) {
continue;
}
var el = this[selectorName];
if (el == null) {
continue;
}
delete this[selectorName];
if (removeOnlySelectors === true) {
continue;
}
el.destroy();
}
},
/**
* The method preprocesses the control's rendering template. Implemented for parameterization
* {@link #styles inline-styles} and {@link #classes CSS classes} of the component.
* @param {String / String []} tpl The template for rendering the control
* @param {Object} tplData - Parameter object of the rendering template
* @protected
* @return {String}
*/
processTemplate: function (tpl, tplData) {
var template = tpl;
if (Ext.isArray(template)) {
template = template.join("");
}
this.copySourceToTplData("styles", tplData);
this.copySourceToTplData("classes", tplData);
/*jshint quotmark:false */
//jscs: disable
template = this.processTpl(this.styleRegexTemplate, template, tplData, function (style) {
var stylesString = Ext.isString(style) ? style : Ext.DomHelper.generateStyles(style);
return 'style="' + stylesString + '"';
});
template = this.processTpl(this.cssRegexTemplate, template, tplData, function (cssClass) {
var classString = Ext.isArray(cssClass) ? cssClass.join(" ") : cssClass;
return 'class="' + classString + '"';
});
//jscs: enable
/*jshint quotmark:double */
template = this.encodeHtmlTemplate(template);
return template;
},
/**
* Encodes specified symbols.
* @param {String} template Control render template.
* @return {String} Encoded control render template.
*/
encodeHtmlTemplate: function (template) {
if (Ext.isString(template)) {
template = template.replace(/\s+/g, " ");
template = template.replace(/\\u/g, "\u");
template = template.replace(/\\x/g, "\x");
}
return template;
},
/**
* The method of initializing the component. In the base implementation, the
* {@link #afterrender afterrender} event is subscribed. When overriding it is necessary to call the base method.
* @protected
*/
init: function () {
this.on("afterrender", this.onAfterRender, this);
this.on("afterrerender", this.onAfterReRender, this);
this.on("beforererender", this.onBeforeReRender, this);
},
/**
* Fires canExecute event with resume method.
* @protected
* @param {Object} config
* @param {Function} config.method Check method.
* @param {Mixed} config.args Arguments of the check method.
* @return {Boolean} True if allowed to continue execute check method.
*/
canExecute: function (config) {
var args = config.args;
var event = args[args.length - 1];
if (event && event.isComeBack) {
return true;
}
Array.prototype.push.call(config.args, {
isComeBack: true
});
Ext.apply(config, { scope: this });
var canExecute = this.fireEvent("canExecute", config);
return canExecute;
},
/**
* The {@link #afterrender afterrender} event handler. Sets the flags that the component is rendered and starts the
* {@link # selectors of selectors} mechanism. After setting the references to the DOM elements, it calls the
* function to subscribe to the DOM events.
* @param {Terrasoft.Component} component Component.
* @param {Ext.dom.Element} containerEl DOM element in which the component was rendered.
* @protected
*/
onAfterRender: function (component, containerEl) {
this.rendered = true;
this.rendering = false;
this.applySelectors(containerEl);
this.initDomEvents();
this.addWrapAttr();
this.setDomAttributes(this.domAttributes);
if (this.observeMutations) {
this.initMutationObserver();
}
},
/**
* The {@link #beforererender} event handler. Sets the flag that selectors must be bound to a model
* @protected
*/
onBeforeReRender: function () {
this.selectorsWereBind = false;
},
/**
* Adds a marker data-item-marker to the control.
* @protected
*/
addWrapAttr: function () {
var markerValue = this.markerValue;
var wrapEl;
if (!Ext.isEmpty(markerValue) && (wrapEl = this.getWrapEl())) {
wrapEl.dom.setAttribute("data-item-marker", Terrasoft.encodeHtml(markerValue));
}
},
/**
* The {@link #afterrender afterrerender} event handler. Sets the flags that the component is rendered and starts
* the {@link # selectors of selectors} mechanism. After setting the references to the DOM elements, it calls the
* function to subscribe to the DOM events.
* @param {Terrasoft.Component} component Component.
* @param {Ext.dom.Element} containerEl DOM element in which the component was rendered.
* @protected
*/
onAfterReRender: function (component, containerEl) {
this.rendered = true;
this.rendering = false;
this.applySelectors(containerEl);
this.initDomEvents();
this.addWrapAttr();
this.setDomAttributes(this.domAttributes);
if (this.observeMutations) {
this.initMutationObserver();
}
},
/**
* The method checks for the presence of the {@link #selectors of selectors} object, as well as the presence of
* the wrapEl selector.
* If no selector is found, an exception will be thrown. Called before the selector mechanism is used.
* If the class heir needs to guarantee the presence of a certain selector, then the method must be redefined.
* For example, for the MyTextComponent class to work, you must have a link to the text input field.
*
* Ext.define('Terrasoft.MyTextComponent', {
* extend: 'Terrasoft.Component',
* className: 'mycomponent',
* ...
* getSelectors: function() {
* this.selectors = {
* wrapEl: '#' + this.id,
* el: '#' + this.id + '-editor'
* }
* },
* verifySelectors: function() {
* var result = this.callParent(arguments);
* var selectors = this.selectors;
* if (!selectors.el) {
* return false;
* }
* return result;
* }
* });
*
* @protected
* @return {Boolean}
*/
verifySelectors: function () {
var selectors = this.selectors;
var result = false;
if (selectors && selectors.wrapEl) {
result = true;
}
return result;
},
/**
* The method is implemented to subscribe to DOM events for the controls.
* @protected
*/
initDomEvents: function () {
this.mixins.expandableTip.initDomEvents.call(this);
},
/**
* The method deletes the subscribers to the DOM events of the controls.
* In the base implementation, the events of the elements that were received using
* the {@link #selectors of selectors} mechanism are deleted.
* @protected
*/
clearDomListeners: function () {
var selectors = this.selectors;
for (var selectorName in selectors) {
if (!selectors.hasOwnProperty(selectorName)) {
continue;
}
var el = this[selectorName];
if (el == null) {
continue;
}
Ext.EventManager.removeAll(el);
}
this.selectorsWereBind = false;
this.mixins.expandableTip.clearDomListeners.call(this);
},
/**
* The method prepares a rendering template. Creates references for inline methods
* @param {Ext.XTemplate} tpl Template for processing.
* @protected
*/
initRenderTemplate: function (tpl) {
tpl.renderComponent = this.renderComponent;
},
/**
* The method prepares a parameter object for rendering the control. A link to the component is installed.
* @param {Object} renderData parameter object for processing.
* @protected
*/
initRenderData: function (renderData) {
renderData.self = this;
},
/**
* The method returns a rendering template for the control. The base implementation returns the property
* of the {@link #tpl tpl} component. Class heirs must override the method and return the current template.
* @return {String[]}
* @protected
*/
getTpl: function () {
return this.tpl || "";
},
/**
* The method returns the parameter object of the control's rendering template.
* The base implementation returns the property of the {@link #tplData tplData} component supplemented with the
* id parameters and a reference to the self component.
* Class successors must override the method and return the current parameter object.
* @return {Object}
* @protected
*/
getTplData: function () {
var tplData = {
id: this.id,
self: this,
tabIndex: this.tabIndex,
direction: this.direction
};
return Ext.apply(tplData, this.tplData || {});
},
/**
* Returns the configuration of the binding to the model. Implements the {@link Terrasoft.Bindable} mixin interface.
*/
getBindConfig: function () {
var bindConfig = {
visible: {
changeMethod: "setVisible"
},
enabled: {
changeMethod: "setEnabled"
},
maskVisible: {
changeMethod: "setMaskVisible"
},
markerValue: {
changeMethod: "setMarkerValue"
},
domAttributes: {
changeMethod: "setDomAttributes"
}
};
return bindConfig;
},
/**
* The method does a check for the possibility of component rendering. The basic implementation checks only that
* the component is rendered and visible.
* ** Important! ** There is no verification in the {@link #reRender reRender ()} method.
* The developer must call the method on his own, and if the result is true, call the method reRender ().
* @return {Boolean}
* @protected
*/
allowRerender: function () {
return this.visible === true && this.rendered === true;
},
/**
* The method re-renders the control. If the component was not rendered, an exception will be thrown.
* All DOM elements of the component are destroyed during the rendering process, after which the rendering
* engine starts, but with some differences (the {@link #afterrender afterrender} event will not be started).
* @param {Number} index (optional) The position for the component to be re-rendered. Used for compatibility
* with the old version of the re-rendering
* @param {Ext.dom.Element} container (optional) Reference to Ext.Element, where the component will be rendered.
* If the parameter is not specified, an exception will be thrown. Used for compatibility with the old version
* of the re-rendering
* @protected
*/
reRender: function (index, container) {
if (!Ext.isEmpty(index) || !Ext.isEmpty(container)) {
this.deprecatedReRender(index, container);
return;
}
this.rendering = true;
this.fireEvent("beforeRerender", this);
var wrapEl = this.getWrapEl();
if (Ext.isEmpty(wrapEl)) {
if (this.ownerCt.allowRerender()) {
this.ownerCt.reRender();
}
return;
}
this.clearDomListeners();
this.removeElementsBySelectors(true);
var html = this.generateHtml();
var parent = wrapEl.parent();
var domNode = Ext.DomHelper.append(parent, html);
wrapEl.dom.parentNode.replaceChild(domNode, wrapEl.dom);
var el = Ext.get(domNode);
this.fireEvent("afterrerender", this, el);
},
/**
* Rerenders control if it visible and rendered.
*/
safeRerender: function () {
if (this.allowRerender()) {
this.reRender.apply(this, arguments);
}
},
/**
* {@inheritdoc #reRender}
* Deprecated implementation of the method. Used for compatibility
* @deprecated
*/
deprecatedReRender: function (index, container) {
this.rendering = true;
this.fireEvent("beforeRerender", this);
var wrapEl = this.getWrapEl();
var wrapDom = wrapEl ? wrapEl.dom : {};
var prevNode = wrapDom.previousSibling;
var ownerCt = this.ownerCt;
var ownerCtEl = ownerCt ? ownerCt.getRenderToEl(this) : null;
var containerEl = container ? container.dom : ownerCt ? ownerCtEl.dom : wrapDom.parentNode;
var insertPosition = "beforeend";
var referenceNode = containerEl;
var visibleNodes = [];
if (index != null) {
if (index < 0) {
throw Terrasoft.ArgumentNullOrEmptyException();
}
if (container) {
if (index === 0) {
insertPosition = "afterbegin";
} else {
visibleNodes = Terrasoft.getVisibleDomItems(containerEl.childNodes, [wrapDom]);
referenceNode = visibleNodes[index - 1];
insertPosition = "afterend";
}
} else {
if (index === 0) {
prevNode = null;
} else {
visibleNodes = Terrasoft.getVisibleDomItems(containerEl.childNodes, [wrapDom]);
prevNode = visibleNodes[index - 1];
}
if (prevNode === wrapDom) {
prevNode = prevNode.nextSibling;
}
}
}
if (!container) {
referenceNode = prevNode == null ? containerEl : prevNode;
insertPosition = prevNode == null ? "afterbegin" : "afterend";
}
this.clearDomListeners();
this.removeElementsBySelectors();
var html = this.generateHtml();
Ext.DomHelper.insertHtml(insertPosition, referenceNode, html);
this.fireEvent("afterrerender", this, containerEl);
},
/**
* The event handler {@link #beforererender beforererender} of the parent component.
* In the method, the component signs off from the DOM events and destroys the view.
*/
onOwnerCtBeforeRerender: function () {
this.fireEvent("beforererender", this);
if (this.rendered) {
this.clearDomListeners();
this.removeElementsBySelectors();
this.rendered = false;
}
},
/**
* The event handler {@link #afterrender afterrender} and the {@link #afterrerender afterrerender} of the parent
* component. The method starts its own rendering events to start the mechanism {@link # selectors of selectors},
* in order for the component to generate links to its DOM elements after the rendering.
* @param {String} eventName The name of the event to be processed.
*/
onOwnerCtAfterRenderOrAfterRerender: function (eventName) {
if (this.rendering !== true) {
return;
}
if (!this.visible) {
return;
}
var ownerCtEl = this.ownerCt.getWrapEl();
this.fireEvent(eventName, this, ownerCtEl);
},
/**
* The method is called by the {@link Terrasoft.Container} container into which the component is added.
* In the method, a subscription to container rendering events is performed to start its rendering events.
* If the container is rendered, the {@link #render render ()} method is called.
* The reference to the handler object of the parent component is also stored.
* @param {Terrasoft.Container} ownerCt The container in which the component was added.
* @protected
*/
added: function (ownerCt) {
this.ownerCt = ownerCt;
this.fireEvent("added", this, ownerCt);
this.ownerCtListeners = ownerCt.on({
beforererender: this.onOwnerCtBeforeRerender,
afterrender: {
fn: this.onOwnerCtAfterRenderOrAfterRerender.bind(this, "afterrender"),
scope: this
},
afterrerender: this.onOwnerCtAfterRenderOrAfterRerender.bind(this, "afterrerender"),
scope: this,
destroyable: true
});
if (ownerCt.rendered === true) {
var index = ownerCt.indexOf(this);
var renderEl = ownerCt.getRenderToEl(this);
this.render(renderEl, index);
}
},
/**
* The method is called by the {@link Terrasoft.Container} container from which the component is removed.
* In the method, there is an unsubscription from the container rendering events and a nulling
* of the {@link #ownerCt} property.
* @protected
*/
removed: function () {
this.ownerCt = null;
var ownerCtListeners = this.ownerCtListeners;
ownerCtListeners.destroy();
},
/**
* In the method, the component's rendering template is prepared to generate HTML markup.
* Because for the work of the {@link Ext.XTemplate Ext.XTemplate} class it is necessary that all the
* inline functions used in the template are in the instance of the Ext.XTemplate object, then there is a link
* to these functions from the template parameter object, tplData.
* @param {Ext.XTemplate} tpl Rendering Template
* @param {Object} tplData Template parameter object
* @protected
*/
prepareTpl: function (tpl, tplData) {
for (var propertyName in tplData) {
if (!tplData.hasOwnProperty(propertyName)) {
continue;
}
var property = tplData[propertyName];
if (Ext.isFunction(property)) {
tpl[propertyName] = property;
}
}
},
/**
* The method destroys the control. The component is removed from the component manager, all subscribers
* are deleted, both for the component and for the DOM elements. All DOM elements are deleted.
* @override
*/
onDestroy: function () {
var ownerCt = this.ownerCt;
if (ownerCt) {
ownerCt.remove(this);
this.ownerCt = null;
this.ownerCtListeners.destroy();
}
this.fireEvent("destroy", this);
this.mixins.bindable.destroy.call(this);
this.mixins.expandableTip.destroy.call(this);
Terrasoft.ComponentManager.unregister(this);
this.clearListeners();
if (this.rendered) {
this.clearDomListeners();
this.removeElementsBySelectors();
}
this.rendered = false;
this.callParent(arguments);
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
},
/**
* The method returns a reference to the DOM element wrapper component (see {@link #wrapEl wrapEl}).
* @return {Ext.dom.Element}
*/
getWrapEl: function () {
return this.wrapEl;
},
/**
* The method starts the rendering of the control.
* @param {Ext.dom.Element} container A reference to the Ext.Element where the component will be rendered.
* If the parameter is not specified, an exception will be thrown.
* @param {Number} [index] The rendering position. Indicates optional.
*/
render: function (container, index) {
if (!container) {
throw new Terrasoft.ArgumentNullOrEmptyException();
}
if (!container.dom) {
throw new Terrasoft.UnsupportedTypeException();
}
if (this.rendered === true) {
var messageTemplate = Terrasoft.Resources.Controls.Component.ComponentIsAlreadyRendered;
var errorMessage = Terrasoft.getFormattedString(messageTemplate, this.id);
this.log(errorMessage, Terrasoft.LogMessageType.ERROR);
return;
}
if (this.visible !== true) {
return;
}
var containerDom = container.dom;
var html = this.generateHtml();
var insertPosition = "beforeend";
var referenceNode = containerDom;
if (index != null) {
if (index < 0) {
throw Terrasoft.ArgumentNullOrEmptyException();
}
if (index === 0) {
referenceNode = containerDom;
insertPosition = "afterbegin";
} else {
var nodes = containerDom.childNodes;
referenceNode = nodes[index - 1];
insertPosition = "afterend";
if (!referenceNode) {
if (nodes.length === 0) {
referenceNode = containerDom;
insertPosition = "afterbegin";
} else {
referenceNode = nodes[nodes.length - 1];
}
}
}
}
Ext.DomHelper.insertHtml(insertPosition, referenceNode, html);
this.fireEvent("afterrender", this, container);
},
/**
* The method returns the HTML markup of the control.
* @return {String}
*/
generateHtml: function () {
if (this.visible !== true) {
return "";
}
var generateHtml = [];
this.rendering = true;
var renderConfig = this.generateRenderConfig();
renderConfig.tpl.applyOut(renderConfig.tplData, generateHtml);
return generateHtml.join("");
},
/**
* Returns owner component property name.
* @return {String} Owner component property name.
*/
getOwnerCtViewItemsPropertyName: function () {
return this.ownerCtPropertyName;
},
/**
* Sets owner component property name.
* @param {String} value Owner component property name.
*/
setOwnerCtViewItemsPropertyName: function (value) {
this.ownerCtPropertyName = value;
},
/**
* Sets dom attributes.
* @param {Object} attributes Dom attributes.
*/
setDomAttributes: function (attributes) {
if (attributes && !Ext.isEmpty(attributes["data-item-marker"])) {
throw new Terrasoft.InvalidOperationException();
}
var attributesToSet = {};
Terrasoft.each(Object.keys(this.domAttributes || {}), function (attribute) {
attributesToSet[attribute] = undefined;
}, this);
Ext.apply(attributesToSet, attributes);
this.domAttributes = attributes;
if (this.wrapEl) {
this.wrapEl.set(attributesToSet);
}
},
/**
* Returns dom attributes.
* @return {Object} Returns dom attributes/
*/
getDomAttributes: function () {
return this.domAttributes;
},
/**
* The method enables or disables the control.
* @param {Boolean} enabled
*/
setEnabled: function (enabled) {
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
var disabledClass = this.disabledClass;
var wrapEl = this.getWrapEl();
if (wrapEl && disabledClass && this.rendered) {
if (enabled) {
wrapEl.removeCls(disabledClass);
} else {
wrapEl.addCls(disabledClass);
}
}
this.fireEvent("enabledchanged", enabled, this);
},
/**
* Calculate text direction and update direction property.
* @protected
* @param {String} text Text for which it is necessary to calculate the direction.
*/
updateDirection: function (text) {
var direction = Terrasoft.getTextDirection(text);
this.direction = direction;
},
/**
* Checks the visibility of a component based on its parent elements.
* @return {Boolean}
*/
isVisible: function () {
if (this.visible === false || this.rendered === false) {
return false;
}
if (this.ownerCt && !this.ownerCt.isVisible()) {
return false;
}
var wrapEl = this.getWrapEl();
return wrapEl.isVisible(true);
},
/**
* Hides or shows the control.
* If the component was hidden and un-rendered, then when the component.setVisible (true) is called for the control,
* rendering is started.
* @param {Boolean} visible
*/
setVisible: function (visible) {
if (this.visible === visible) {
return;
}
this.visible = visible;
if (visible === true) {
var ownerCt = this.ownerCt;
var container;
var index = null;
if (ownerCt) {
container = ownerCt.getRenderToEl(this);
index = ownerCt.indexOf(this);
} else {
container = this.renderTo;
}
if (this.rendered === false) {
if (container) {
if (index === -1) {
index = null;
}
this.render(container, index);
}
} else {
this.reRender();
}
} else if (this.rendered === true) {
var wrapEl = this.getWrapEl();
if (wrapEl) {
wrapEl.dom.style.display = visible ? "" : "none";
}
}
this.fireEvent("visiblechanged", this, visible);
},
/**
* The method returns a reference to the DOM element where the elements will be displayed.
* @return {Ext.dom.Element}
*/
getRenderToEl: function () {
return this.getWrapEl();
},
/**
* The method returns a reference to the DOM element where the elements will be displayed.
* {@link #setMarkerValue}.
* @param {String} markerValue Marker value.
*/
setMarkerValue: function (markerValue) {
if (this.markerValue === markerValue) {
return;
}
this.markerValue = markerValue;
if (this.rendered) {
this.addWrapAttr();
}
},
/**
* The method displays or hides the mask.
* The method uses the static class Terrasoft.Mask. The mask identifier is stored in the maskId property.
* The mask is superimposed on the element returned by the {@link #getMaskEl} method.
* The mask configuration is generated using the {@link #getMaskConfig} method.
* @param {Boolean} maskVisible
*/
setMaskVisible: function (maskVisible) {
if (this.maskVisible === maskVisible || !this.rendered) {
// TODO #CRM-30568 Fix mask visibility after component render
return;
}
this.maskVisible = maskVisible;
if (maskVisible) {
var maskConfig = this.getMaskConfig();
this.maskId = Terrasoft.Mask.show(maskConfig);
} else {
Terrasoft.Mask.hide(this.maskId);
this.maskId = null;
}
},
/**
* Returns the element for masking.
* @protected
* @return {Ext.dom.Element}
*/
getMaskEl: function () {
return this.getWrapEl();
},
/**
* Returns an object mask configuration to be applied to the component. Used by method
* {@link #setMaskVisible}. Selector generates method {@link #getMaskEl}.
* @protected
* @return {Object}
* - **selector** String: Selector DOM-element
* - **caption** String: Header
*/
getMaskConfig: function () {
var maskEl = this.getMaskEl();
var maskConfig = {
selector: "#" + maskEl.id,
caption: "",
frameVisible: false
};
if (this.maskTimeout !== null) {
maskConfig.timeout = this.maskTimeout;
}
return maskConfig;
},
/**
* Validates binding context.
* @param {Terrasoft.BaseViewModel} model Model.
*/
validateBindingContext: function (model) {
if (this.className !== "Terrasoft.controls.Component" && this.model !== model) {
this.log(Terrasoft.Resources.Controls.Component.BindingContextWarningMessage);
}
},
/**
* Binds the control to the model.
* Initializes the property with initial data and subscribes to changes to the corresponding properties
* or methods of the model.
* For correct bindingContext use this.model instead of model argument for item bindings.
* @param {Terrasoft.BaseViewModel} model Model.
*/
bind: function (model) {
this.mixins.bindable.bind.call(this, model);
this.mixins.expandableTip.bind.call(this, this.model);
this.validateBindingContext(model);
},
/**
* Clears timer by it's name.
* @param {String} timerName Name of the timer.
* @return {Boolean} True, if timer was active.
*/
clearTimer: function (timerName) {
var timerId = this[timerName];
if (timerId) {
clearTimeout(timerId);
this[timerName] = null;
}
return Boolean(timerId);
},
/**
* Checks whether the event target is a children of component dom.
* @param {Event} event Event to checks.
* @return {Boolean} True in the case of a positive result.
*/
isEventWithin: function (event) {
var result = false;
var wrapEl = this.getWrapEl();
if (wrapEl) {
result = event.within(wrapEl);
}
return result;
}
});