/**
* Provides precise pixel measurements for blocks of text so that you can determine
* the exact pixel height and width of a block of text.
*
* **Note:** The TextMetrics tool should only be utilized to measure plain text. Attempting to
* measure text that includes HTML may return inaccurate results.
*
* This measurement works by copying the relevant font-related CSS styles from the element
* param to the TextMetrics' cached measurement element. This returns the dimensions of the cached
* element wrapping the text. By default, the wrapping element is auto-sized.
* You must provide a **fixed width** if the passed text is multi-lined.
*
* When multiple measurements are being done with the same element styling, you should
* create a single, reusable TextMetrics instance. This is more efficient than using the
* static {@link #measure} method. The element styles are copied to the cached
* TextMetrics element once during instantiation versus repeated copying using
* _measure()_.
*
* The following example demonstrates the recommended use of TextMetrics where the custom
* textfield class sets up a reusable TextMetrics instance used to measure the label
* width. This example assumes that all instances of _mytextfield_ have the same
* {@link Ext.form.Labelable#labelClsExtra labelClsExtra} and
* {@link Ext.form.Labelable#labelStyle labelStyle} configs.
*
* Ext.define('MyApp.view.MyTextField', {
* extend: 'Ext.form.field.Text',
* xtype: 'mytextfield',
*
* initComponent: function () {
* var me = this,
* tm = me.getTextMetrics();
*
* me.labelWidth = tm.getWidth(me.fieldLabel + me.labelSeparator);
* me.callParent();
* },
*
* getTextMetrics: function () {
* var me = this,
* // Using me.self allows labelCls etc. to vary by derived
* // class, but not by instance.
* cls = me.self,
* tm = cls.measurer,
* el;
*
* if (!tm) {
* el = Ext.getBody().createChild();
* el.addCls(me.labelCls + ' ' + me.labelClsExtra).
* applyStyles(me.labelStyle);
*
* cls.measurer = tm = new Ext.util.TextMetrics(el);
* }
*
* return tm;
* }
* });
*
* Ext.create('Ext.form.Panel', {
* title: 'Contact Info',
* width: 600,
* bodyPadding: 10,
* renderTo: Ext.getBody(),
* items: [{
* xtype: 'mytextfield',
* fieldLabel: 'Name',
* labelStyle: 'font-size: 10px;'
* }, {
* xtype: 'mytextfield',
* fieldLabel: 'Email Address',
* labelStyle: 'font-size: 10px;'
* }]
* });
*
* While less efficient than the preceding example, this example allows each instance of
* _mytextfield2_ to have unique labelClsExtra and labelStyle configs. Each custom
* textfield instance uses the static TextMetrics measure method which will copy the
* label styles repeatedly, thus being less efficient but more versatile.
*
* Ext.define('MyApp.view.MyTextField2', {
* extend: 'Ext.form.field.Text',
* xtype: 'mytextfield2',
*
* initComponent: function () {
* var me = this,
* el = me.getMeasurementEl(),
* tm = Ext.util.TextMetrics;
*
* me.labelWidth = tm.measure(el, me.fieldLabel + me.labelSeparator).width;
* me.callParent();
* },
*
* getMeasurementEl: function () {
* var me = this,
* cls = MyApp.view.MyTextField2,
* el = cls.measureEl;
*
* if (!el) {
* cls.measureEl = el = Ext.getBody().createChild();
* }
*
* el.dom.removeAttribute('style');
* el.removeCls(el.dom.className).
* addCls(me.labelCls + ' ' + me.labelClsExtra).
* applyStyles(me.labelStyle);
*
* return el;
* }
* });
*
* Ext.create('Ext.form.Panel', {
* title: 'Contact Info',
* width: 600,
* bodyPadding: 10,
* renderTo: Ext.getBody(),
* items: [{
* xtype: 'mytextfield2',
* fieldLabel: 'Name',
* labelStyle: 'font-size: 14px;font-weight: bold;',
* labelClsExtra: 'nameLabel'
* }, {
* xtype: 'mytextfield2',
* fieldLabel: 'Email Address',
* labelStyle: 'font-size: 10px;',
* labelClsExtra: 'emailLabel'
* }]
* });
*/
Ext.define('Ext.util.TextMetrics', {
requires: [
'Ext.dom.Element'
],
statics: {
shared: null,
/**
* Measures the size of the specified text
* @param {String/HTMLElement} el The element, dom node or id from which to copy existing
* CSS styles that can affect the size of the rendered text
* @param {String} text The text to measure
* @param {Number} [fixedWidth] If the text will be multiline, you have to set a fixed width
* in order to accurately measure the text height
* @return {Object} An object containing the text's size
* `{width: (width), height: (height)}`
* @static
*/
measure: function(el, text, fixedWidth) {
var me = this,
shared = me.shared || (me.shared = new me(el, fixedWidth));
shared.bind(el);
shared.setFixedWidth(fixedWidth || 'auto');
return shared.getSize(text);
},
/**
* Destroy the TextMetrics instance created by {@link #measure}.
* @static
*/
destroy: function() {
this.shared = Ext.destroy(this.shared);
}
},
/**
* Creates new TextMetrics.
* @param {String/HTMLElement/Ext.dom.Element} bindTo The element or its ID to bind to.
* @param {Number} [fixedWidth] A fixed width to apply to the measuring element.
*/
constructor: function(bindTo, fixedWidth) {
var me = this,
measure = me.measure = Ext.getBody().createChild({
//<debug>
// tell the spec runner to ignore this element when checking if the dom is clean
'data-sticky': true,
//</debug>
role: 'presentation',
cls: Ext.baseCSSPrefix + 'textmetrics',
style: {
position: 'absolute',
left: '-1000px',
top: '-1000px',
visibility: 'hidden'
}
});
if (bindTo) {
me.bind(bindTo);
}
if (fixedWidth) {
measure.setWidth(fixedWidth);
}
},
/**
* Returns the size of the specified text based on the internal element's style and width
* properties
* @param {String} text The text to measure
* @return {Object} An object containing the text's size `{width: (width), height: (height)}`
*/
getSize: function(text) {
var measure = this.measure,
size;
measure.setHtml(text);
size = measure.getSize();
measure.setHtml('');
return size;
},
/**
* Binds this TextMetrics instance to a new element
* @param {String/HTMLElement/Ext.dom.Element} el The element or its ID.
*/
bind: function(el) {
this.measure.setStyle(
// Create an Ext.dom.Fly instance on our prototype unless we've already been through
// here. Attach it to the passed HTMLElement/Ext.Element
(this.el || (this.self.prototype.el = new Ext.dom.Fly())).attach(el).getStyle([
'font-size',
'font-size-adjust',
'font-style',
'font-weight',
'font-family',
'font-kerning',
'font-stretch',
'line-height',
'text-transform',
'text-decoration',
'letter-spacing',
'word-break'
])
);
},
/**
* Sets a fixed width on the internal measurement element. If the text will be multiline,
* you have to set a fixed width in order to accurately measure the text height.
* @param {Number} width The width to set on the element
*/
setFixedWidth: function(width) {
this.measure.setWidth(width);
},
/**
* Returns the measured width of the specified text
* @param {String} text The text to measure
* @return {Number} width The width in pixels
*/
getWidth: function(text) {
this.measure.dom.style.width = 'auto';
return this.getSize(text).width;
},
/**
* Returns the measured height of the specified text
* @param {String} text The text to measure
* @return {Number} height The height in pixels
*/
getHeight: function(text) {
return this.getSize(text).height;
},
/**
* Destroy this instance
*/
destroy: function() {
var me = this;
me.el = me.measure = Ext.destroy(me.measure);
me.callParent();
}
}, function() {
Ext.Element.override({
/**
* Returns the width in pixels of the passed text, or the width of the text in this Element.
* @param {String} text The text to measure. Defaults to the innerHTML of the element.
* @param {Number} [min] The minumum value to return.
* @param {Number} [max] The maximum value to return.
* @return {Number} The text width in pixels.
* @member Ext.dom.Element
*/
getTextWidth: function(text, min, max) {
return Ext.Number.constrain(
Ext.util.TextMetrics.measure(
this.dom, Ext.valueFrom(text, this.dom.innerHTML, true)
).width,
min || 0,
max || 1000000
);
}
});
});