(function () { var clone = fabric.util.object.clone; /** * Textbox class, based on IText, allows the user to resize the text rectangle * and wraps lines automatically. Textboxes have their Y scaling locked, the * user can only change width. Height is adjusted automatically based on the * wrapping of lines. * @class fabric.Textbox * @extends fabric.IText * @mixes fabric.Observable * @return {fabric.Textbox} thisArg * @see {@link fabric.Textbox#initialize} for constructor definition */ fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, { /** * Type of an object * @type String * @default */ type: 'textbox', /** * Minimum width of textbox, in pixels. * @type Number * @default */ minWidth: 20, /** * Minimum calculated width of a textbox, in pixels. * @type Number * @default */ dynamicMinWidth: 0, /** * Cached array of text wrapping. * @type Array */ __cachedLines: null, /** * Constructor. Some scaling related property values are forced. Visibility * of controls is also fixed; only the rotation and width controls are * made available. * @param {String} text Text string * @param {Object} [options] Options object * @return {fabric.Textbox} thisArg */ initialize: function (text, options) { this.ctx = fabric.util.createCanvasElement().getContext('2d'); this.callSuper('initialize', text, options); this.set({ lockUniScaling: false, lockScalingY: true, lockScalingFlip: true, hasBorders: true }); this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); // add width to this list of props that effect line wrapping. this._dimensionAffectingProps.width = true; }, /** * Unlike superclass's version of this function, Textbox does not update * its width. * @param {CanvasRenderingContext2D} ctx Context to use for measurements * @private * @override */ _initDimensions: function (ctx) { if (this.__skipDimension) { return; } if (!ctx) { ctx = fabric.util.createCanvasElement().getContext('2d'); this._setTextStyles(ctx); } // clear dynamicMinWidth as it will be different after we re-wrap line this.dynamicMinWidth = 0; // wrap lines this._textLines = this._splitTextIntoLines(); // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap if (this.dynamicMinWidth > this.width) { this._set('width', this.dynamicMinWidth); } // calculate a styleMap that lets us know where styles as, as _textLines is separated by \n and wraps, // but the style object line indices is by \n. this._styleMap = this._generateStyleMap(); // clear cache and re-calculate height this._clearCache(); this.height = this._getTextHeight(ctx); }, _generateStyleMap: function () { var realLineCount = 0, realLineCharCount = 0, charCount = 0, map = {}; for (var i = 0; i < this._textLines.length; i++) { if (this.text[charCount] === '\n') { realLineCharCount = 0; charCount++; realLineCount++; } else if (this.text[charCount] === ' ') { // this case deals with space's that are removed from end of lines when wrapping realLineCharCount++; charCount++; } map[i] = {line: realLineCount, offset: realLineCharCount}; charCount += this._textLines[i].length; realLineCharCount += this._textLines[i].length; } return map; }, /** * @param {Number} lineIndex * @param {Number} charIndex * @param {Boolean} [returnCloneOrEmpty=false] * @private */ _getStyleDeclaration: function (lineIndex, charIndex, returnCloneOrEmpty) { if (this._styleMap) { var map = this._styleMap[lineIndex]; lineIndex = map.line; charIndex = map.offset + charIndex; } 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) { var map = this._styleMap[lineIndex]; lineIndex = map.line; charIndex = map.offset + charIndex; this.styles[lineIndex][charIndex] = style; }, /** * @param {Number} lineIndex * @param {Number} charIndex * @private */ _deleteStyleDeclaration: function (lineIndex, charIndex) { var map = this._styleMap[lineIndex]; lineIndex = map.line; charIndex = map.offset + charIndex; delete this.styles[lineIndex][charIndex]; }, /** * @param {Number} lineIndex * @private */ _getLineStyle: function (lineIndex) { var map = this._styleMap[lineIndex]; return this.styles[map.line]; }, /** * @param {Number} lineIndex * @param {Object} style * @private */ _setLineStyle: function (lineIndex, style) { var map = this._styleMap[lineIndex]; this.styles[map.line] = style; }, /** * @param {Number} lineIndex * @private */ _deleteLineStyle: function (lineIndex) { var map = this._styleMap[lineIndex]; delete this.styles[map.line]; }, /** * Wraps text using the 'width' property of Textbox. First this function * splits text on newlines, so we preserve newlines entered by the user. * Then it wraps each line using the width of the Textbox by calling * _wrapLine(). * @param {CanvasRenderingContext2D} ctx Context to use for measurements * @param {String} text The string of text that is split into lines * @returns {Array} Array of lines */ _wrapText: function (ctx, text) { var lines = text.split(this._reNewline), wrapped = [], i; for (i = 0; i < lines.length; i++) { wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i)); } return wrapped; }, /** * Helper function to measure a string of text, given its lineIndex and charIndex offset * * @param {CanvasRenderingContext2D} ctx * @param {String} text * @param {number} lineIndex * @param {number} charOffset * @returns {number} * @private */ _measureText: function (ctx, text, lineIndex, charOffset) { var width = 0, decl; charOffset = charOffset || 0; for (var i = 0; i < text.length; i++) { if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i + charOffset])) { ctx.save(); width += this._applyCharStylesGetWidth(ctx, text[i], lineIndex, i, decl); ctx.restore(); } else { // @note: we intentionally pass in an empty style declaration, because if we pass in nothing, it will // retry fetching style declaration width += this._applyCharStylesGetWidth(ctx, text[i], lineIndex, i, {}); } } return width; }, /** * Wraps a line of text using the width of the Textbox and a context. * @param {CanvasRenderingContext2D} ctx Context to use for measurements * @param {String} text The string of text to split into lines * @param {Number} lineIndex * @returns {Array} Array of line(s) into which the given text is wrapped * to. */ _wrapLine: function (ctx, text, lineIndex) { var maxWidth = this.width, lineWidth = this._measureText(ctx, text, lineIndex, 0); // first case: does the whole line fit? if (lineWidth < maxWidth) { // if the current line is only one word, we need to keep track of it if it's a large word if (text.indexOf(' ') === -1 && lineWidth > this.dynamicMinWidth) { this.dynamicMinWidth = lineWidth; } return [text]; } // if the whole line doesn't fit, we break it up into words var lines = [], line = '', words = text.split(' '), offset = 0, infix = '', wordWidth = 0, largestWordWidth = 0; while (words.length > 0) { infix = line === '' ? '' : ' '; wordWidth = this._measureText(ctx, words[0], lineIndex, line.length + infix.length + offset); lineWidth = line === '' ? wordWidth : this._measureText(ctx, line + infix + words[0], lineIndex, offset); if (lineWidth < maxWidth || (line === '' && wordWidth >= maxWidth)) { line += infix + words.shift(); } else { offset += line.length + 1; // add 1 because each word is separated by a space lines.push(line); line = ''; } if (words.length === 0) { lines.push(line); } // keep track of largest word if (wordWidth > largestWordWidth) { largestWordWidth = wordWidth; } } if (largestWordWidth > this.dynamicMinWidth) { this.dynamicMinWidth = largestWordWidth; } return lines; }, /** * Gets lines of text to render in the Textbox. This function calculates * text wrapping on the fly everytime it is called. * @returns {Array} Array of lines in the Textbox. * @override */ _splitTextIntoLines: function () { this.ctx.save(); this._setTextStyles(this.ctx); var lines = this._wrapText(this.ctx, this.text); this.ctx.restore(); return lines; }, /** * When part of a group, we don't want the Textbox's scale to increase if * the group's increases. That's why we reduce the scale of the Textbox by * the amount that the group's increases. This is to maintain the effective * scale of the Textbox at 1, so that font-size values make sense. Otherwise * the same font-size value would result in different actual size depending * on the value of the scale. * @param {String} key * @param {Any} value */ setOnGroup: function (key, value) { if (key === 'scaleX') { this.set('scaleX', Math.abs(1 / value)); this.set('width', (this.get('width') * value) / (typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX)); this.__oldScaleX = value; } }, /** * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start). * Overrides the superclass function to take into account text wrapping. * * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. */ get2DCursorLocation: function (selectionStart) { if (typeof selectionStart === 'undefined') { selectionStart = this.selectionStart; } var numLines = this._textLines.length, removed = 0; for (var i = 0; i < numLines; i++) { var line = this._textLines[i], lineLen = line.length; if (selectionStart <= removed + lineLen) { return { lineIndex: i, charIndex: selectionStart - removed }; } removed += lineLen; if (this.text[removed] === '\n' || this.text[removed] === ' ') { removed++; } } return { lineIndex: numLines - 1, charIndex: this._textLines[numLines - 1].length }; }, /** * Overrides superclass function and uses text wrapping data to get cursor * boundary offsets instead of the array of chars. * @param {Array} chars Unused * @param {String} typeOfBoundaries Can be 'cursor' or 'selection' * @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set. */ _getCursorBoundariesOffsets: function (chars, typeOfBoundaries) { var topOffset = 0, leftOffset = 0, cursorLocation = this.get2DCursorLocation(), lineChars = this._textLines[cursorLocation.lineIndex].split(''), lineLeftOffset = this._getCachedLineOffset(cursorLocation.lineIndex); for (var i = 0; i < cursorLocation.charIndex; i++) { leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i); } for (i = 0; i < cursorLocation.lineIndex; i++) { topOffset += this._getHeightOfLine(this.ctx, i); } if (typeOfBoundaries === 'cursor') { topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex) / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex) * (1 - this._fontSizeFraction); } return { top: topOffset, left: leftOffset, lineLeft: lineLeftOffset }; }, getMinWidth: function () { return Math.max(this.minWidth, this.dynamicMinWidth); }, /** * 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), { minWidth: this.minWidth }); } }); /** * Returns fabric.Textbox instance from an object representation * @static * @memberOf fabric.Textbox * @param {Object} object Object to create an instance from * @return {fabric.Textbox} instance of fabric.Textbox */ fabric.Textbox.fromObject = function (object) { return new fabric.Textbox(object.text, clone(object)); }; /** * Returns the default controls visibility required for Textboxes. * @returns {Object} */ fabric.Textbox.getTextboxControlVisibility = function () { return { tl: false, tr: false, br: false, bl: false, ml: true, mt: false, mr: true, mb: false, mtr: true }; }; /** * Contains all fabric.Textbox objects that have been created * @static * @memberOf fabric.Textbox * @type Array */ fabric.Textbox.instances = []; })();