(function() { var clone = fabric.util.object.clone; /** * IText class (introduced in v1.4) Events are also fired with "text:" * prefix when observing canvas. * @class fabric.IText * @extends fabric.Text * @mixes fabric.Observable * * @fires changed * @fires selection:changed * @fires editing:entered * @fires editing:exited * * @return {fabric.IText} thisArg * @see {@link fabric.IText#initialize} for constructor definition * *
Supported key combinations:
** Move cursor: left, right, up, down * Select character: shift + left, shift + right * Select text vertically: shift + up, shift + down * Move cursor by word: alt + left, alt + right * Select words: shift + alt + left, shift + alt + right * Move cursor to line start/end: cmd + left, cmd + right or home, end * Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end * Jump to start/end of text: cmd + up, cmd + down * Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown * Delete character: backspace * Delete word: alt + backspace * Delete line: cmd + backspace * Forward delete: delete * Copy text: ctrl/cmd + c * Paste text: ctrl/cmd + v * Cut text: ctrl/cmd + x * Select entire text: ctrl/cmd + a * Quit editing tab or esc ** *
Supported mouse/touch combination
** Position cursor: click/touch * Create selection: click/touch & drag * Create selection: click & shift + click * Select word: double click * Select line: triple click **/ fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { /** * Type of an object * @type String * @default */ type: 'i-text', /** * Index where text selection starts (or where cursor is when there is no selection) * @type Number * @default */ selectionStart: 0, /** * Index where text selection ends * @type Number * @default */ selectionEnd: 0, /** * Color of text selection * @type String * @default */ selectionColor: 'rgba(17,119,255,0.3)', /** * Indicates whether text is in editing mode * @type Boolean * @default */ isEditing: false, /** * Indicates whether a text can be edited * @type Boolean * @default */ editable: true, /** * Border color of text object while it's in editing mode * @type String * @default */ editingBorderColor: 'rgba(102,153,255,0.25)', /** * Width of cursor (in px) * @type Number * @default */ cursorWidth: 2, /** * Color of default cursor (when not overwritten by character style) * @type String * @default */ cursorColor: '#333', /** * Delay between cursor blink (in ms) * @type Number * @default */ cursorDelay: 1000, /** * Duration of cursor fadein (in ms) * @type Number * @default */ cursorDuration: 600, /** * Object containing character styles * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) * @type Object * @default */ styles: null, /** * Indicates whether internal text char widths can be cached * @type Boolean * @default */ caching: true, /** * @private * @type Boolean * @default */ _skipFillStrokeCheck: false, /** * @private */ _reSpace: /\s|\n/, /** * @private */ _currentCursorOpacity: 0, /** * @private */ _selectionDirection: null, /** * @private */ _abortCursorAnimation: false, /** * @private */ _charWidthsCache: { }, /** * @private */ __widthOfSpace: [ ], /** * Constructor * @param {String} text Text string * @param {Object} [options] Options object * @return {fabric.IText} thisArg */ initialize: function(text, options) { this.styles = options ? (options.styles || { }) : { }; this.callSuper('initialize', text, options); this.initBehavior(); }, /** * @private */ _clearCache: function() { this.callSuper('_clearCache'); this.__widthOfSpace = [ ]; }, /** * Returns true if object has no styling */ isEmptyStyles: function() { if (!this.styles) { return true; } var obj = this.styles; for (var p1 in obj) { for (var p2 in obj[p1]) { /*jshint unused:false */ for (var p3 in obj[p1][p2]) { return false; } } } return true; }, /** * Sets selection start (left boundary of a selection) * @param {Number} index Index to set selection start to */ setSelectionStart: function(index) { index = Math.max(index, 0); if (this.selectionStart !== index) { this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); this.selectionStart = index; } this._updateTextarea(); }, /** * Sets selection end (right boundary of a selection) * @param {Number} index Index to set selection end to */ setSelectionEnd: function(index) { index = Math.min(index, this.text.length); if (this.selectionEnd !== index) { this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); this.selectionEnd = index; } this._updateTextarea(); }, /** * Gets style of a current selection/cursor (at the start position) * @param {Number} [startIndex] Start index to get styles at * @param {Number} [endIndex] End index to get styles at * @return {Object} styles Style object at a specified (or current) index */ getSelectionStyles: function(startIndex, endIndex) { if (arguments.length === 2) { var styles = [ ]; for (var i = startIndex; i < endIndex; i++) { styles.push(this.getSelectionStyles(i)); } return styles; } var loc = this.get2DCursorLocation(startIndex), style = this._getStyleDeclaration(loc.lineIndex, loc.charIndex); return style || {}; }, /** * Sets style of a current selection * @param {Object} [styles] Styles object * @return {fabric.IText} thisArg * @chainable */ setSelectionStyles: function(styles) { if (this.selectionStart === this.selectionEnd) { this._extendStyles(this.selectionStart, styles); } else { for (var i = this.selectionStart; i < this.selectionEnd; i++) { this._extendStyles(i, styles); } } /* not included in _extendStyles to avoid clearing cache more than once */ this._forceClearCache = true; return this; }, /** * @private */ _extendStyles: function(index, styles) { var loc = this.get2DCursorLocation(index); if (!this._getLineStyle(loc.lineIndex)) { this._setLineStyle(loc.lineIndex, {}); } if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); } fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { this.callSuper('_render', ctx); this.ctx = ctx; this.isEditing && this.renderCursorOrSelection(); }, /** * Renders cursor or selection (depending on what exists) */ renderCursorOrSelection: function() { if (!this.active) { return; } var chars = this.text.split(''), boundaries, ctx; if (this.canvas.contextTop) { ctx = this.canvas.contextTop; ctx.save(); ctx.transform.apply(ctx, this.canvas.viewportTransform); this.transform(ctx); this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix); } else { ctx = this.ctx; ctx.save(); } if (this.selectionStart === this.selectionEnd) { boundaries = this._getCursorBoundaries(chars, 'cursor'); this.renderCursor(boundaries, ctx); } else { boundaries = this._getCursorBoundaries(chars, 'selection'); this.renderSelection(chars, boundaries, ctx); } ctx.restore(); }, /** * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. */ get2DCursorLocation: function(selectionStart) { if (typeof selectionStart === 'undefined') { selectionStart = this.selectionStart; } var len = this._textLines.length; for (var i = 0; i < len; i++) { if (selectionStart <= this._textLines[i].length) { return { lineIndex: i, charIndex: selectionStart }; } selectionStart -= this._textLines[i].length + 1; } return { lineIndex: i - 1, charIndex: this._textLines[i - 1].length < selectionStart ? this._textLines[i - 1].length : selectionStart }; }, /** * Returns complete style of char at the current cursor * @param {Number} lineIndex Line index * @param {Number} charIndex Char index * @return {Object} Character style */ getCurrentCharStyle: function(lineIndex, charIndex) { var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); return { fontSize: style && style.fontSize || this.fontSize, fill: style && style.fill || this.fill, textBackgroundColor: style && style.textBackgroundColor || this.textBackgroundColor, textDecoration: style && style.textDecoration || this.textDecoration, fontFamily: style && style.fontFamily || this.fontFamily, fontWeight: style && style.fontWeight || this.fontWeight, fontStyle: style && style.fontStyle || this.fontStyle, stroke: style && style.stroke || this.stroke, strokeWidth: style && style.strokeWidth || this.strokeWidth }; }, /** * Returns fontSize of char at the current cursor * @param {Number} lineIndex Line index * @param {Number} charIndex Char index * @return {Number} Character font size */ getCurrentCharFontSize: function(lineIndex, charIndex) { var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); return style && style.fontSize ? style.fontSize : this.fontSize; }, /** * Returns color (fill) of char at the current cursor * @param {Number} lineIndex Line index * @param {Number} charIndex Char index * @return {String} Character color (fill) */ getCurrentCharColor: function(lineIndex, charIndex) { var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); return style && style.fill ? style.fill : this.cursorColor; }, /** * Returns cursor boundaries (left, top, leftOffset, topOffset) * @private * @param {Array} chars Array of characters * @param {String} typeOfBoundaries */ _getCursorBoundaries: function(chars, typeOfBoundaries) { // left/top are left/top of entire text box // leftOffset/topOffset are offset from that left/top point of a text box var left = Math.round(this._getLeftOffset()), top = this._getTopOffset(), offsets = this._getCursorBoundariesOffsets( chars, typeOfBoundaries); return { left: left, top: top, leftOffset: offsets.left + offsets.lineLeft, topOffset: offsets.top }; }, /** * @private */ _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { var lineLeftOffset = 0, lineIndex = 0, charIndex = 0, topOffset = 0, leftOffset = 0; for (var i = 0; i < this.selectionStart; i++) { if (chars[i] === '\n') { leftOffset = 0; topOffset += this._getHeightOfLine(this.ctx, lineIndex); lineIndex++; charIndex = 0; } else { leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); charIndex++; } lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)); } if (typeOfBoundaries === 'cursor') { topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, lineIndex) / this.lineHeight - this.getCurrentCharFontSize(lineIndex, charIndex) * (1 - this._fontSizeFraction); } return { top: topOffset, left: leftOffset, lineLeft: lineLeftOffset }; }, /** * Renders cursor * @param {Object} boundaries * @param {CanvasRenderingContext2D} ctx transformed context to draw on */ renderCursor: function(boundaries, ctx) { var cursorLocation = this.get2DCursorLocation(), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex, charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), leftOffset = (lineIndex === 0 && charIndex === 0) ? this._getLineLeftOffset(this._getLineWidth(ctx, lineIndex)) : boundaries.leftOffset; ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; ctx.fillRect( boundaries.left + leftOffset, boundaries.top + boundaries.topOffset, this.cursorWidth / this.scaleX, charHeight); }, /** * Renders text selection * @param {Array} chars Array of characters * @param {Object} boundaries Object with left/top/leftOffset/topOffset * @param {CanvasRenderingContext2D} ctx transformed context to draw on */ renderSelection: function(chars, boundaries, ctx) { ctx.fillStyle = this.selectionColor; var start = this.get2DCursorLocation(this.selectionStart), end = this.get2DCursorLocation(this.selectionEnd), startLine = start.lineIndex, endLine = end.lineIndex; for (var i = startLine; i <= endLine; i++) { var lineOffset = this._getLineLeftOffset(this._getLineWidth(ctx, i)) || 0, lineHeight = this._getHeightOfLine(this.ctx, i), boxWidth = 0, line = this._textLines[i]; if (i === startLine) { for (var j = 0, len = line.length; j < len; j++) { if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) { boxWidth += this._getWidthOfChar(ctx, line[j], i, j); } if (j < start.charIndex) { lineOffset += this._getWidthOfChar(ctx, line[j], i, j); } } } else if (i > startLine && i < endLine) { boxWidth += this._getLineWidth(ctx, i) || 5; } else if (i === endLine) { for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { boxWidth += this._getWidthOfChar(ctx, line[j2], i, j2); } } ctx.fillRect( boundaries.left + lineOffset, boundaries.top + boundaries.topOffset, boxWidth, lineHeight); boundaries.topOffset += lineHeight; } }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderChars: function(method, ctx, line, left, top, lineIndex, charOffset) { if (this.isEmptyStyles()) { return this._renderCharsFast(method, ctx, line, left, top); } charOffset = charOffset || 0; this.skipTextAlign = true; // set proper box offset left -= this.textAlign === 'center' ? (this.width / 2) : (this.textAlign === 'right') ? this.width : 0; // set proper line offset var lineHeight = this._getHeightOfLine(ctx, lineIndex), lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(ctx, lineIndex)), prevStyle, thisStyle, charsToRender = ''; left += lineLeftOffset || 0; ctx.save(); top -= lineHeight / this.lineHeight * this._fontSizeFraction; for (var i = charOffset, len = line.length + charOffset; i <= len; i++) { prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i); thisStyle = this.getCurrentCharStyle(lineIndex, i + 1); if (this._hasStyleChanged(prevStyle, thisStyle) || i === len) { this._renderChar(method, ctx, lineIndex, i - 1, charsToRender, left, top, lineHeight); charsToRender = ''; prevStyle = thisStyle; } charsToRender += line[i - charOffset]; } ctx.restore(); }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line Content of the line * @param {Number} left Left coordinate * @param {Number} top Top coordinate */ _renderCharsFast: function(method, ctx, line, left, top) { this.skipTextAlign = false; if (method === 'fillText' && this.fill) { this.callSuper('_renderChars', method, ctx, line, left, top); } if (method === 'strokeText' && ((this.stroke && this.strokeWidth > 0) || this.skipFillStrokeCheck)) { this.callSuper('_renderChars', method, ctx, line, left, top); } }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex * @param {Number} i * @param {String} _char * @param {Number} left Left coordinate * @param {Number} top Top coordinate * @param {Number} lineHeight Height of the line */ _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { var charWidth, charHeight, shouldFill, shouldStroke, decl = this._getStyleDeclaration(lineIndex, i), offset, textDecoration; if (decl) { charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i); shouldStroke = decl.stroke; shouldFill = decl.fill; textDecoration = decl.textDecoration; } else { charHeight = this.fontSize; } shouldStroke = shouldStroke || method === 'strokeText'; shouldFill = shouldFill || method === 'fillText'; decl && ctx.save(); charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || {}); textDecoration = textDecoration || this.textDecoration; shouldFill && ctx.fillText(_char, left, top); shouldStroke && ctx.strokeText(_char, left, top); if (textDecoration || textDecoration !== '') { offset = this._fontSizeFraction * lineHeight / this.lineHeight; this._renderCharDecoration(ctx, textDecoration, left, top, offset, charWidth, charHeight); } decl && ctx.restore(); ctx.translate(charWidth, 0); }, /** * @private * @param {Object} prevStyle * @param {Object} thisStyle */ _hasStyleChanged: function(prevStyle, thisStyle) { return (prevStyle.fill !== thisStyle.fill || prevStyle.fontSize !== thisStyle.fontSize || prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || prevStyle.textDecoration !== thisStyle.textDecoration || prevStyle.fontFamily !== thisStyle.fontFamily || prevStyle.fontWeight !== thisStyle.fontWeight || prevStyle.fontStyle !== thisStyle.fontStyle || prevStyle.stroke !== thisStyle.stroke || prevStyle.strokeWidth !== thisStyle.strokeWidth ); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderCharDecoration: function(ctx, textDecoration, left, top, offset, charWidth, charHeight) { if (!textDecoration) { return; } var decorationWeight = charHeight / 15, positions = { 'underline': top + charHeight / 10, 'line-through': top - charHeight * (this._fontSizeFraction + this._fontSizeMult - 1) + decorationWeight, 'overline': top - (this._fontSizeMult - this._fontSizeFraction) * charHeight }, decorations = ['underline', 'line-through', 'overline'], i, decoration; for (i = 0; i < decorations.length; i++) { decoration = decorations[i]; if (textDecoration.indexOf(decoration) > -1) { ctx.fillRect(left, positions[decoration], charWidth , decorationWeight); } } }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine // the adding 0.03 is just to align text with itext by overlap test if (!this.isEmptyStyles()) { top += this.fontSize * (this._fontSizeFraction + 0.03); } this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextDecoration: function(ctx) { if (this.isEmptyStyles()) { return this.callSuper('_renderTextDecoration', ctx); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextLinesBackground: function(ctx) { if (!this.textBackgroundColor && !this.styles) { return; } ctx.save(); if (this.textBackgroundColor) { ctx.fillStyle = this.textBackgroundColor; } var lineHeights = 0; for (var i = 0, len = this._textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i); if (this._textLines[i] === '') { lineHeights += heightOfLine; continue; } var lineWidth = this._getLineWidth(ctx, i), lineLeftOffset = this._getLineLeftOffset(lineWidth); if (this.textBackgroundColor) { ctx.fillStyle = this.textBackgroundColor; ctx.fillRect( this._getLeftOffset() + lineLeftOffset, this._getTopOffset() + lineHeights, lineWidth, heightOfLine / this.lineHeight ); } if (this._getLineStyle(i)) { for (var j = 0, jlen = this._textLines[i].length; j < jlen; j++) { var style = this._getStyleDeclaration(i, j); if (style && style.textBackgroundColor) { var _char = this._textLines[i][j]; ctx.fillStyle = style.textBackgroundColor; ctx.fillRect( this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j), this._getTopOffset() + lineHeights, this._getWidthOfChar(ctx, _char, i, j) + 1, heightOfLine / this.lineHeight ); } } } lineHeights += heightOfLine; } ctx.restore(); }, /** * @private */ _getCacheProp: function(_char, styleDeclaration) { return _char + styleDeclaration.fontFamily + styleDeclaration.fontSize + styleDeclaration.fontWeight + styleDeclaration.fontStyle + styleDeclaration.shadow; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} _char * @param {Number} lineIndex * @param {Number} charIndex * @param {Object} [decl] */ _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { var charDecl = this._getStyleDeclaration(lineIndex, charIndex), styleDeclaration = decl || clone(charDecl), width; this._applyFontStyles(styleDeclaration); var cacheProp = this._getCacheProp(_char, styleDeclaration); // short-circuit if no styles for this char // global style from object is always applyed and handled by save and restore if (!charDecl && this._charWidthsCache[cacheProp] && this.caching) { return this._charWidthsCache[cacheProp]; } if (typeof styleDeclaration.shadow === 'string') { styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); } var fill = styleDeclaration.fill || this.fill; ctx.fillStyle = fill.toLive ? fill.toLive(ctx, this) : fill; if (styleDeclaration.stroke) { ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) ? styleDeclaration.stroke.toLive(ctx, this) : styleDeclaration.stroke; } ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; ctx.font = this._getFontDeclaration.call(styleDeclaration); this._setShadow.call(styleDeclaration, ctx); if (!this.caching || !this._charWidthsCache[cacheProp]) { width = ctx.measureText(_char).width; this.caching && (this._charWidthsCache[cacheProp] = width); } return this._charWidthsCache[cacheProp]; }, /** * @private * @param {Object} styleDeclaration */ _applyFontStyles: function(styleDeclaration) { if (!styleDeclaration.fontFamily) { styleDeclaration.fontFamily = this.fontFamily; } if (!styleDeclaration.fontSize) { styleDeclaration.fontSize = this.fontSize; } if (!styleDeclaration.fontWeight) { styleDeclaration.fontWeight = this.fontWeight; } if (!styleDeclaration.fontStyle) { styleDeclaration.fontStyle = this.fontStyle; } }, /** * @param {Number} lineIndex * @param {Number} charIndex * @param {Boolean} [returnCloneOrEmpty=false] * @private */ _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) { if (returnCloneOrEmpty) { return (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) ? clone(this.styles[lineIndex][charIndex]) : { }; } return this.styles[lineIndex] && this.styles[lineIndex][charIndex] ? this.styles[lineIndex][charIndex] : null; }, /** * @param {Number} lineIndex * @param {Number} charIndex * @param {Object} style * @private */ _setStyleDeclaration: function(lineIndex, charIndex, style) { this.styles[lineIndex][charIndex] = style; }, /** * * @param {Number} lineIndex * @param {Number} charIndex * @private */ _deleteStyleDeclaration: function(lineIndex, charIndex) { delete this.styles[lineIndex][charIndex]; }, /** * @param {Number} lineIndex * @private */ _getLineStyle: function(lineIndex) { return this.styles[lineIndex]; }, /** * @param {Number} lineIndex * @param {Object} style * @private */ _setLineStyle: function(lineIndex, style) { this.styles[lineIndex] = style; }, /** * @param {Number} lineIndex * @private */ _deleteLineStyle: function(lineIndex) { delete this.styles[lineIndex]; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { if (this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) { return this._getWidthOfSpace(ctx, lineIndex); } var styleDeclaration = this._getStyleDeclaration(lineIndex, charIndex, true); this._applyFontStyles(styleDeclaration); var cacheProp = this._getCacheProp(_char, styleDeclaration); if (this._charWidthsCache[cacheProp] && this.caching) { return this._charWidthsCache[cacheProp]; } else if (ctx) { ctx.save(); var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); ctx.restore(); return width; } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getHeightOfChar: function(ctx, lineIndex, charIndex) { var style = this._getStyleDeclaration(lineIndex, charIndex); return style && style.fontSize ? style.fontSize : this.fontSize; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfCharsAt: function(ctx, lineIndex, charIndex) { var width = 0, i, _char; for (i = 0; i < charIndex; i++) { _char = this._textLines[lineIndex][i]; width += this._getWidthOfChar(ctx, _char, lineIndex, i); } return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getLineWidth: function(ctx, lineIndex) { if (this.__lineWidths[lineIndex]) { return this.__lineWidths[lineIndex]; } this.__lineWidths[lineIndex] = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length); return this.__lineWidths[lineIndex]; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex */ _getWidthOfSpace: function (ctx, lineIndex) { if (this.__widthOfSpace[lineIndex]) { return this.__widthOfSpace[lineIndex]; } var line = this._textLines[lineIndex], wordsWidth = this._getWidthOfWords(ctx, line, lineIndex), widthDiff = this.width - wordsWidth, numSpaces = line.length - line.replace(this._reSpacesAndTabs, '').length, width = widthDiff / numSpaces; this.__widthOfSpace[lineIndex] = width; return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} line * @param {Number} lineIndex */ _getWidthOfWords: function (ctx, line, lineIndex) { var width = 0; for (var charIndex = 0; charIndex < line.length; charIndex++) { var _char = line[charIndex]; if (!_char.match(/\s/)) { width += this._getWidthOfChar(ctx, _char, lineIndex, charIndex); } } return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getHeightOfLine: function(ctx, lineIndex) { if (this.__lineHeights[lineIndex]) { return this.__lineHeights[lineIndex]; } var line = this._textLines[lineIndex], maxHeight = this._getHeightOfChar(ctx, lineIndex, 0); for (var i = 1, len = line.length; i < len; i++) { var currentCharHeight = this._getHeightOfChar(ctx, lineIndex, i); if (currentCharHeight > maxHeight) { maxHeight = currentCharHeight; } } this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; return this.__lineHeights[lineIndex]; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getTextHeight: function(ctx) { var height = 0; for (var i = 0, len = this._textLines.length; i < len; i++) { height += this._getHeightOfLine(ctx, i); } return height; }, /** * This method is overwritten to account for different top offset * @private */ _renderTextBoxBackground: function(ctx) { if (!this.backgroundColor) { return; } ctx.save(); ctx.fillStyle = this.backgroundColor; ctx.fillRect( this._getLeftOffset(), this._getTopOffset(), this.width, this.height ); ctx.restore(); }, /** * Returns object representation of an instance * @method toObject * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { styles: clone(this.styles) }); } }); /** * Returns fabric.IText instance from an object representation * @static * @memberOf fabric.IText * @param {Object} object Object to create an instance from * @return {fabric.IText} instance of fabric.IText */ fabric.IText.fromObject = function(object) { return new fabric.IText(object.text, clone(object)); }; })();