/** * @class Ext.draw.sprite.Text * @extends Ext.draw.sprite.Sprite * * A sprite that represents text. * * @example * Ext.create({ * xtype: 'draw', * renderTo: document.body, * width: 600, * height: 400, * sprites: [{ * type: 'text', * x: 50, * y: 50, * text: 'Sencha', * fontSize: 30, * fillStyle: '#1F6D91' * }] * }); *//* eslint-disable indent */Ext.define('Ext.draw.sprite.Text', function() { // Absolute font sizes. var fontSizes = { 'xx-small': true, 'x-small': true, 'small': true, 'medium': true, 'large': true, 'x-large': true, 'xx-large': true }, fontWeights = { normal: true, bold: true, bolder: true, lighter: true, 100: true, 200: true, 300: true, 400: true, 500: true, 600: true, 700: true, 800: true, 900: true }, textAlignments = { start: 'start', left: 'start', center: 'center', middle: 'center', end: 'end', right: 'end' }, textBaselines = { top: 'top', hanging: 'hanging', middle: 'middle', center: 'middle', alphabetic: 'alphabetic', ideographic: 'ideographic', bottom: 'bottom' }; return { extend: 'Ext.draw.sprite.Sprite', requires: [ 'Ext.draw.TextMeasurer', 'Ext.draw.Color' ], alias: 'sprite.text', type: 'text', lineBreakRe: /\r?\n/g, //<debug> statics: { /** * Debug rendering options: * * debug: { * bbox: true // renders the bounding box of the text sprite * } * */ debug: false, fontSizes: fontSizes, fontWeights: fontWeights, textAlignments: textAlignments, textBaselines: textBaselines }, //</debug> inheritableStatics: { def: { animationProcessors: { text: 'text' }, processors: { /** * @cfg {Number} [x=0] * The position of the sprite on the x-axis. */ x: 'number', /** * @cfg {Number} [y=0] * The position of the sprite on the y-axis. */ y: 'number', /** * @cfg {String} [text=''] * The text represented in the sprite. */ text: 'string', /** * @cfg {String/Number} [fontSize='10px'] * The size of the font displayed. */ fontSize: function(n) { // Numbers as strings will be converted to numbers, // null will be converted to 0. if (Ext.isNumber(+n)) { return n + 'px'; } else if (n.match(Ext.dom.Element.unitRe)) { return n; } else if (n in fontSizes) { return n; } }, /** * @cfg {String} [fontStyle=''] * The style of the font displayed. {normal, italic, oblique} */ fontStyle: 'enums(,italic,oblique)', /** * @cfg {String} [fontVariant=''] * The variant of the font displayed. {normal, small-caps} */ fontVariant: 'enums(,small-caps)', /** * @cfg {String} [fontWeight=''] * The weight of the font displayed. {normal, bold, bolder, lighter} */ fontWeight: function(n) { if (n in fontWeights) { return String(n); } else { return ''; } }, /** * @cfg {String} [fontFamily='sans-serif'] * The family of the font displayed. */ fontFamily: 'string', /** * @cfg {String} [textAlign='start'] * The alignment of the text displayed. * {left, right, center, start, end} */ textAlign: function(n) { return textAlignments[n] || 'center'; }, /** * @cfg {String} [textBaseline="alphabetic"] * The baseline of the text displayed. * {top, hanging, middle, alphabetic, ideographic, bottom} */ textBaseline: function(n) { return textBaselines[n] || 'alphabetic'; }, /** * @cfg {String} [font='10px sans-serif'] * The font displayed. */ font: 'string', //<debug> debug: 'default' //</debug> }, aliases: { 'font-size': 'fontSize', 'font-family': 'fontFamily', 'font-weight': 'fontWeight', 'font-variant': 'fontVariant', 'text-anchor': 'textAlign', 'dominant-baseline': 'textBaseline' }, defaults: { fontStyle: '', fontVariant: '', fontWeight: '', fontSize: '10px', fontFamily: 'sans-serif', font: '10px sans-serif', textBaseline: 'alphabetic', textAlign: 'start', strokeStyle: 'rgba(0, 0, 0, 0)', fillStyle: '#000', x: 0, y: 0, text: '' }, triggers: { fontStyle: 'fontX,bbox', fontVariant: 'fontX,bbox', fontWeight: 'fontX,bbox', fontSize: 'fontX,bbox', fontFamily: 'fontX,bbox', font: 'font,bbox,canvas', textBaseline: 'bbox', textAlign: 'bbox', x: 'bbox', y: 'bbox', text: 'bbox' }, updaters: { fontX: 'makeFontShorthand', font: 'parseFontShorthand' } } }, config: { /** * @private * If the value is boolean, it overrides the TextMeasurer's 'precise' config * (for the given sprite only). */ preciseMeasurement: undefined }, constructor: function(config) { var key; if (config && config.font) { config = Ext.clone(config); for (key in config) { if (key !== 'font' && key.indexOf('font') === 0) { delete config[key]; } } } Ext.draw.sprite.Sprite.prototype.constructor.call(this, config); }, // Maps values to font properties they belong to. fontValuesMap: { // Skip 'normal' and 'inherit' values, as the first one // is the default and the second one has no meaning in Canvas. 'italic': 'fontStyle', 'oblique': 'fontStyle', 'small-caps': 'fontVariant', 'bold': 'fontWeight', 'bolder': 'fontWeight', 'lighter': 'fontWeight', '100': 'fontWeight', '200': 'fontWeight', '300': 'fontWeight', '400': 'fontWeight', '500': 'fontWeight', '600': 'fontWeight', '700': 'fontWeight', '800': 'fontWeight', '900': 'fontWeight', // Absolute font sizes. 'xx-small': 'fontSize', 'x-small': 'fontSize', 'small': 'fontSize', 'medium': 'fontSize', 'large': 'fontSize', 'x-large': 'fontSize', 'xx-large': 'fontSize' // Relative font sizes like 'smaller' and 'larger' // have no meaning, and are not included. }, makeFontShorthand: function(attr) { var parts = []; if (attr.fontStyle) { parts.push(attr.fontStyle); } if (attr.fontVariant) { parts.push(attr.fontVariant); } if (attr.fontWeight) { parts.push(attr.fontWeight); } if (attr.fontSize) { parts.push(attr.fontSize); } if (attr.fontFamily) { parts.push(attr.fontFamily); } this.setAttributes({ font: parts.join(' ') }, true); }, // For more info see: // http://www.w3.org/TR/CSS21/fonts.html#font-shorthand parseFontShorthand: function(attr) { var value = attr.font, ln = value.length, changes = {}, dispatcher = this.fontValuesMap, start = 0, end, slashIndex, part, fontProperty; while (start < ln && end !== -1) { end = value.indexOf(' ', start); if (end < 0) { part = value.substr(start); } else if (end > start) { part = value.substr(start, end - start); } else { continue; } // Since Canvas fillText doesn't support multi-line text, // it is assumed that line height is never specified, i.e. // in entries like these the part after slash is omitted: // 12px/14px sans-serif // x-large/110% "New Century Schoolbook", serif slashIndex = part.indexOf('/'); if (slashIndex > 0) { part = part.substr(0, slashIndex); } else if (slashIndex === 0) { continue; } // All optional font properties (fontStyle, fontVariant or fontWeight) can be 'normal'. // They can go in any order. Which ones are 'normal' is determined by elimination. // E.g. if only fontVariant is specified, then 'normal' applies to fontStyle // and fontWeight. // If none are explicitly mentioned, then all are 'normal'. if (part !== 'normal' && part !== 'inherit') { fontProperty = dispatcher[part]; if (fontProperty) { changes[fontProperty] = part; } else if (part.match(Ext.dom.Element.unitRe)) { changes.fontSize = part; } else { // Assuming that font family always goes last in the font shorthand. changes.fontFamily = value.substr(start); break; } } start = end + 1; } if (!changes.fontStyle) { changes.fontStyle = ''; // same as 'normal' } if (!changes.fontVariant) { changes.fontVariant = ''; // same as 'normal' } if (!changes.fontWeight) { changes.fontWeight = ''; // same as 'normal' } this.setAttributes(changes, true); }, fontProperties: { fontStyle: true, fontVariant: true, fontWeight: true, fontSize: true, fontFamily: true }, setAttributes: function(changes, bypassNormalization, avoidCopy) { var key, obj; // Discard individual font properties if 'font' shorthand was also provided. // Example: a user provides a config for chart series labels, using the font // shorthand, which is parsed into individual font properties and corresponding // sprite attributes are set. Then a theme is applied to the chart, and // individual font properties from the theme make up the new font shorthand // that overrides the previous one. In other words, no matter what font // the user has specified, theme font will be used. // This workaround relies on the fact that the theme merges its own config with // the user config (where user config values take over the same theme config // values). So both user font shorthand and individual font properties from // the theme are present in the resulting config (since there are no collisions), // which ends up here as the 'changes' parameter. // If the user wants their font config to merged with the the theme's font config, // instead of taking over it, individual font properties should be used // by the user as well. if (changes && changes.font) { obj = {}; for (key in changes) { if (!(key in this.fontProperties)) { obj[key] = changes[key]; } } changes = obj; } this.callParent([changes, bypassNormalization, avoidCopy]); }, // Overriding the getBBox method of the abstract sprite here to always // recalculate the bounding box of the text in flipped RTL mode // because in that case the position of the sprite depends not just on // the value of its 'x' attribute, but also on the width of the surface // the sprite belongs to. getBBox: function(isWithoutTransform) { var me = this, plain = me.attr.bbox.plain, surface = me.getSurface(); //<debug> // The sprite's bounding box won't account for RTL if it doesn't // belong to a surface. // if (!surface) { // Ext.raise("The sprite does not belong to a surface."); // } //</debug> if (plain.dirty) { me.updatePlainBBox(plain); plain.dirty = false; } if (surface && surface.getInherited().rtl && surface.getFlipRtlText()) { // Since sprite's attributes haven't actually changed at this point, // and we just want to update the position of its bbox // based on surface's width, there's no reason to perform // expensive text measurement operation here, // so we can use the result of the last measurement instead. me.updatePlainBBox(plain, true); } return me.callParent([isWithoutTransform]); }, rtlAlignments: { start: 'end', center: 'center', end: 'start' }, updatePlainBBox: function(plain, useOldSize) { var me = this, attr = me.attr, x = attr.x, y = attr.y, dx = [], font = attr.font, text = attr.text, baseline = attr.textBaseline, alignment = attr.textAlign, precise = me.getPreciseMeasurement(), size, textMeasurerPrecision; if (useOldSize && me.oldSize) { size = me.oldSize; } else { textMeasurerPrecision = Ext.draw.TextMeasurer.precise; if (Ext.isBoolean(precise)) { Ext.draw.TextMeasurer.precise = precise; } size = me.oldSize = Ext.draw.TextMeasurer.measureText(text, font); Ext.draw.TextMeasurer.precise = textMeasurerPrecision; } // eslint-disable-next-line vars-on-top, one-var var surface = me.getSurface(), isRtl = (surface && surface.getInherited().rtl) || false, flipRtlText = isRtl && surface.getFlipRtlText(), sizes = size.sizes, blockHeight = size.height, blockWidth = size.width, ln = sizes ? sizes.length : 0, lineWidth, rect, i = 0; // To get consistent results in all browsers we don't apply textAlign // and textBaseline attributes of the sprite to context, so text is always // left aligned and has an alphabetic baseline. // // Instead we have to calculate the horizontal offset of each line // based on sprite's textAlign, and the vertical offset of the bounding box // based on sprite's textBaseline. // // These offsets are then used by the sprite's 'render' method // to position text properly. switch (baseline) { case 'hanging' : case 'top': break; case 'ideographic' : case 'bottom' : y -= blockHeight; break; case 'alphabetic' : y -= blockHeight * 0.8; break; case 'middle' : y -= blockHeight * 0.5; break; } if (flipRtlText) { rect = surface.getRect(); x = rect[2] - rect[0] - x; alignment = me.rtlAlignments[alignment]; } switch (alignment) { case 'start': if (isRtl) { for (; i < ln; i++) { lineWidth = sizes[i].width; dx.push(-(blockWidth - lineWidth)); } } break; case 'end' : x -= blockWidth; if (isRtl) { break; } for (; i < ln; i++) { lineWidth = sizes[i].width; dx.push(blockWidth - lineWidth); } break; case 'center' : x -= blockWidth * 0.5; for (; i < ln; i++) { lineWidth = sizes[i].width; dx.push((isRtl ? -1 : 1) * (blockWidth - lineWidth) * 0.5); } break; } attr.textAlignOffsets = dx; plain.x = x; plain.y = y; plain.width = blockWidth; plain.height = blockHeight; }, setText: function(text) { this.setAttributes({ text: text }, true); }, render: function(surface, ctx, rect) { var me = this, attr = me.attr, mat = Ext.draw.Matrix.fly(attr.matrix.elements.slice(0)), bbox = me.getBBox(true), dx = attr.textAlignOffsets, none = Ext.util.Color.RGBA_NONE, x, y, i, lines, lineHeight; if (attr.text.length === 0) { return; } lines = attr.text.split(me.lineBreakRe); lineHeight = bbox.height / lines.length; // Simulate textBaseline and textAlign. x = attr.bbox.plain.x; // lineHeight * 0.78 is the approximate distance between the top // and the alphabetic baselines y = attr.bbox.plain.y + lineHeight * 0.78; mat.toContext(ctx); if (surface.getInherited().rtl) { // Canvas element in RTL mode automatically flips text alignment. // Here we compensate for that change. // So text is still positioned and aligned as in the LTR mode, // but the direction of the text is RTL. x += attr.bbox.plain.width; } for (i = 0; i < lines.length; i++) { if (ctx.fillStyle !== none) { ctx.fillText(lines[i], x + (dx[i] || 0), y + lineHeight * i); } if (ctx.strokeStyle !== none) { ctx.strokeText(lines[i], x + (dx[i] || 0), y + lineHeight * i); } } //<debug> // eslint-disable-next-line vars-on-top var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug; if (debug) { // This assumes no part of the sprite is rendered after this call. // If it is, we need to re-apply transformations. // But the bounding box is already transformed, so we remove the transformation. this.attr.inverseMatrix.toContext(ctx); debug.bbox && me.renderBBox(surface, ctx); } //</debug> }}; });