';\n },\n\n /**\n * Return the dimension and the zoom level needed to create a cache canvas\n * big enough to host the object to be cached.\n * @private\n * @param {Object} dim.x width of object to be cached\n * @param {Object} dim.y height of object to be cached\n * @return {Object}.width width of canvas\n * @return {Object}.height height of canvas\n * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache\n * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache\n */\n _getCacheCanvasDimensions: function() {\n var dims = this.callSuper('_getCacheCanvasDimensions');\n var fontSize = this.fontSize;\n dims.width += fontSize * dims.zoomX;\n dims.height += fontSize * dims.zoomY;\n return dims;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _render: function(ctx) {\n this._setTextStyles(ctx);\n if (this.group && this.group.type === 'path-group') {\n ctx.translate(this.left, this.top);\n }\n this._renderTextLinesBackground(ctx);\n this._renderText(ctx);\n this._renderTextDecoration(ctx);\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderText: function(ctx) {\n this._renderTextFill(ctx);\n this._renderTextStroke(ctx);\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _setTextStyles: function(ctx) {\n ctx.textBaseline = 'alphabetic';\n ctx.font = this._getFontDeclaration();\n },\n\n /**\n * @private\n * @return {Number} Height of fabric.Text object\n */\n _getTextHeight: function() {\n return this._getHeightOfSingleLine() + (this._textLines.length - 1) * this._getHeightOfLine();\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @return {Number} Maximum width of fabric.Text object\n */\n _getTextWidth: function(ctx) {\n var maxWidth = this._getLineWidth(ctx, 0);\n\n for (var i = 1, len = this._textLines.length; i < len; i++) {\n var currentLineWidth = this._getLineWidth(ctx, i);\n if (currentLineWidth > maxWidth) {\n maxWidth = currentLineWidth;\n }\n }\n return maxWidth;\n },\n\n /**\n * @private\n * @param {String} method Method name (\"fillText\" or \"strokeText\")\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} chars Chars to render\n * @param {Number} left Left position of text\n * @param {Number} top Top position of text\n */\n _renderChars: function(method, ctx, chars, left, top) {\n // remove Text word from method var\n var shortM = method.slice(0, -4), _char, width;\n if (this[shortM].toLive) {\n var offsetX = -this.width / 2 + this[shortM].offsetX || 0,\n offsetY = -this.height / 2 + this[shortM].offsetY || 0;\n ctx.save();\n ctx.translate(offsetX, offsetY);\n left -= offsetX;\n top -= offsetY;\n }\n if (this.charSpacing !== 0) {\n var additionalSpace = this._getWidthOfCharSpacing();\n chars = chars.split('');\n for (var i = 0, len = chars.length; i < len; i++) {\n _char = chars[i];\n width = ctx.measureText(_char).width + additionalSpace;\n ctx[method](_char, left, top);\n left += width > 0 ? width : 0;\n }\n }\n else {\n ctx[method](chars, left, top);\n }\n this[shortM].toLive && ctx.restore();\n },\n\n /**\n * @private\n * @param {String} method Method name (\"fillText\" or \"strokeText\")\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} line Text to render\n * @param {Number} left Left position of text\n * @param {Number} top Top position of text\n * @param {Number} lineIndex Index of a line in a text\n */\n _renderTextLine: function(method, ctx, line, left, top, lineIndex) {\n // lift the line by quarter of fontSize\n top -= this.fontSize * this._fontSizeFraction;\n\n // short-circuit\n var lineWidth = this._getLineWidth(ctx, lineIndex);\n if (this.textAlign !== 'justify' || this.width < lineWidth) {\n this._renderChars(method, ctx, line, left, top, lineIndex);\n return;\n }\n\n // stretch the line\n var words = line.split(/\\s+/),\n charOffset = 0,\n wordsWidth = this._getWidthOfWords(ctx, words.join(' '), lineIndex, 0),\n widthDiff = this.width - wordsWidth,\n numSpaces = words.length - 1,\n spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0,\n leftOffset = 0, word;\n\n for (var i = 0, len = words.length; i < len; i++) {\n while (line[charOffset] === ' ' && charOffset < line.length) {\n charOffset++;\n }\n word = words[i];\n this._renderChars(method, ctx, word, left + leftOffset, top, lineIndex, charOffset);\n leftOffset += this._getWidthOfWords(ctx, word, lineIndex, charOffset) + spaceWidth;\n charOffset += word.length;\n }\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} word\n */\n _getWidthOfWords: function (ctx, word) {\n var width = ctx.measureText(word).width, charCount, additionalSpace;\n if (this.charSpacing !== 0) {\n charCount = word.split('').length;\n additionalSpace = charCount * this._getWidthOfCharSpacing();\n width += additionalSpace;\n }\n return width > 0 ? width : 0;\n },\n\n /**\n * @private\n * @return {Number} Left offset\n */\n _getLeftOffset: function() {\n return -this.width / 2;\n },\n\n /**\n * @private\n * @return {Number} Top offset\n */\n _getTopOffset: function() {\n return -this.height / 2;\n },\n\n /**\n * Returns true because text has no style\n */\n isEmptyStyles: function() {\n return true;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} method Method name (\"fillText\" or \"strokeText\")\n */\n _renderTextCommon: function(ctx, method) {\n\n var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset();\n\n for (var i = 0, len = this._textLines.length; i < len; i++) {\n var heightOfLine = this._getHeightOfLine(ctx, i),\n maxHeight = heightOfLine / this.lineHeight,\n lineWidth = this._getLineWidth(ctx, i),\n leftOffset = this._getLineLeftOffset(lineWidth);\n this._renderTextLine(\n method,\n ctx,\n this._textLines[i],\n left + leftOffset,\n top + lineHeights + maxHeight,\n i\n );\n lineHeights += heightOfLine;\n }\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderTextFill: function(ctx) {\n if (!this.fill && this.isEmptyStyles()) {\n return;\n }\n\n this._renderTextCommon(ctx, 'fillText');\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderTextStroke: function(ctx) {\n if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) {\n return;\n }\n\n if (this.shadow && !this.shadow.affectStroke) {\n this._removeShadow(ctx);\n }\n\n ctx.save();\n this._setLineDash(ctx, this.strokeDashArray);\n ctx.beginPath();\n this._renderTextCommon(ctx, 'strokeText');\n ctx.closePath();\n ctx.restore();\n },\n\n /**\n * @private\n * @return {Number} height of line\n */\n _getHeightOfLine: function() {\n return this._getHeightOfSingleLine() * this.lineHeight;\n },\n\n /**\n * @private\n * @return {Number} height of line without lineHeight\n */\n _getHeightOfSingleLine: function() {\n return this.fontSize * this._fontSizeMult;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderTextLinesBackground: function(ctx) {\n if (!this.textBackgroundColor) {\n return;\n }\n var lineTopOffset = 0, heightOfLine,\n lineWidth, lineLeftOffset, originalFill = ctx.fillStyle;\n\n ctx.fillStyle = this.textBackgroundColor;\n for (var i = 0, len = this._textLines.length; i < len; i++) {\n heightOfLine = this._getHeightOfLine(ctx, i);\n lineWidth = this._getLineWidth(ctx, i);\n if (lineWidth > 0) {\n lineLeftOffset = this._getLineLeftOffset(lineWidth);\n ctx.fillRect(\n this._getLeftOffset() + lineLeftOffset,\n this._getTopOffset() + lineTopOffset,\n lineWidth,\n heightOfLine / this.lineHeight\n );\n }\n lineTopOffset += heightOfLine;\n }\n ctx.fillStyle = originalFill;\n // if there is text background color no\n // other shadows should be casted\n this._removeShadow(ctx);\n },\n\n /**\n * @private\n * @param {Number} lineWidth Width of text line\n * @return {Number} Line left offset\n */\n _getLineLeftOffset: function(lineWidth) {\n if (this.textAlign === 'center') {\n return (this.width - lineWidth) / 2;\n }\n if (this.textAlign === 'right') {\n return this.width - lineWidth;\n }\n return 0;\n },\n\n /**\n * @private\n */\n _clearCache: function() {\n this.__lineWidths = [];\n this.__lineHeights = [];\n },\n\n /**\n * @private\n */\n _shouldClearDimensionCache: function() {\n var shouldClear = this._forceClearCache;\n shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps'));\n if (shouldClear) {\n this.saveState({ propertySet: '_dimensionAffectingProps' });\n this.dirty = true;\n }\n return shouldClear;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex line number\n * @return {Number} Line width\n */\n _getLineWidth: function(ctx, lineIndex) {\n if (this.__lineWidths[lineIndex]) {\n return this.__lineWidths[lineIndex] === -1 ? this.width : this.__lineWidths[lineIndex];\n }\n\n var width, wordCount, line = this._textLines[lineIndex];\n\n if (line === '') {\n width = 0;\n }\n else {\n width = this._measureLine(ctx, lineIndex);\n }\n this.__lineWidths[lineIndex] = width;\n\n if (width && this.textAlign === 'justify') {\n wordCount = line.split(/\\s+/);\n if (wordCount.length > 1) {\n this.__lineWidths[lineIndex] = -1;\n }\n }\n return width;\n },\n\n _getWidthOfCharSpacing: function() {\n if (this.charSpacing !== 0) {\n return this.fontSize * this.charSpacing / 1000;\n }\n return 0;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex line number\n * @return {Number} Line width\n */\n _measureLine: function(ctx, lineIndex) {\n var line = this._textLines[lineIndex],\n width = ctx.measureText(line).width,\n additionalSpace = 0, charCount, finalWidth;\n if (this.charSpacing !== 0) {\n charCount = line.split('').length;\n additionalSpace = (charCount - 1) * this._getWidthOfCharSpacing();\n }\n finalWidth = width + additionalSpace;\n return finalWidth > 0 ? finalWidth : 0;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderTextDecoration: function(ctx) {\n if (!this.textDecoration) {\n return;\n }\n var halfOfVerticalBox = this.height / 2,\n _this = this, offsets = [];\n\n /** @ignore */\n function renderLinesAtOffset(offsets) {\n var i, lineHeight = 0, len, j, oLen, lineWidth,\n lineLeftOffset, heightOfLine;\n\n for (i = 0, len = _this._textLines.length; i < len; i++) {\n\n lineWidth = _this._getLineWidth(ctx, i);\n lineLeftOffset = _this._getLineLeftOffset(lineWidth);\n heightOfLine = _this._getHeightOfLine(ctx, i);\n\n for (j = 0, oLen = offsets.length; j < oLen; j++) {\n ctx.fillRect(\n _this._getLeftOffset() + lineLeftOffset,\n lineHeight + (_this._fontSizeMult - 1 + offsets[j] ) * _this.fontSize - halfOfVerticalBox,\n lineWidth,\n _this.fontSize / 15);\n }\n lineHeight += heightOfLine;\n }\n }\n\n if (this.textDecoration.indexOf('underline') > -1) {\n offsets.push(0.85); // 1 - 3/16\n }\n if (this.textDecoration.indexOf('line-through') > -1) {\n offsets.push(0.43);\n }\n if (this.textDecoration.indexOf('overline') > -1) {\n offsets.push(-0.12);\n }\n if (offsets.length > 0) {\n renderLinesAtOffset(offsets);\n }\n },\n\n /**\n * return font declaration string for canvas context\n * @returns {String} font declaration formatted for canvas context.\n */\n _getFontDeclaration: function() {\n return [\n // node-canvas needs \"weight style\", while browsers need \"style weight\"\n (fabric.isLikelyNode ? this.fontWeight : this.fontStyle),\n (fabric.isLikelyNode ? this.fontStyle : this.fontWeight),\n this.fontSize + 'px',\n (fabric.isLikelyNode ? ('\"' + this.fontFamily + '\"') : this.fontFamily)\n ].join(' ');\n },\n\n /**\n * Renders text instance on a specified context\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Boolean} noTransform\n */\n render: function(ctx, noTransform) {\n // do not render if object is not visible\n if (!this.visible) {\n return;\n }\n if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {\n return;\n }\n if (this._shouldClearDimensionCache()) {\n this._setTextStyles(ctx);\n this._initDimensions(ctx);\n }\n this.callSuper('render', ctx, noTransform);\n },\n\n /**\n * Returns the text as an array of lines.\n * @returns {Array} Lines in the text\n */\n _splitTextIntoLines: function() {\n return this.text.split(this._reNewline);\n },\n\n /**\n * Returns object representation of an instance\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @return {Object} Object representation of an instance\n */\n toObject: function(propertiesToInclude) {\n var additionalProperties = [\n 'text',\n 'fontSize',\n 'fontWeight',\n 'fontFamily',\n 'fontStyle',\n 'lineHeight',\n 'textDecoration',\n 'textAlign',\n 'textBackgroundColor',\n 'charSpacing'\n ].concat(propertiesToInclude);\n return this.callSuper('toObject', additionalProperties);\n },\n\n /* _TO_SVG_START_ */\n /**\n * Returns SVG representation of an instance\n * @param {Function} [reviver] Method for further parsing of svg representation.\n * @return {String} svg representation of an instance\n */\n toSVG: function(reviver) {\n if (!this.ctx) {\n this.ctx = fabric.util.createCanvasElement().getContext('2d');\n }\n var markup = this._createBaseSVGMarkup(),\n offsets = this._getSVGLeftTopOffsets(this.ctx),\n textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);\n this._wrapSVGTextAndBg(markup, textAndBg);\n\n return reviver ? reviver(markup.join('')) : markup.join('');\n },\n\n /**\n * @private\n */\n _getSVGLeftTopOffsets: function(ctx) {\n var lineTop = this._getHeightOfLine(ctx, 0),\n textLeft = -this.width / 2,\n textTop = 0;\n\n return {\n textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0),\n textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0),\n lineTop: lineTop\n };\n },\n\n /**\n * @private\n */\n _wrapSVGTextAndBg: function(markup, textAndBg) {\n var noShadow = true, filter = this.getSvgFilter(),\n style = filter === '' ? '' : ' style=\"' + filter + '\"';\n\n markup.push(\n '\\t\\n',\n textAndBg.textBgRects.join(''),\n '\\t\\t\\n',\n textAndBg.textSpans.join(''),\n '\\t\\t\\n',\n '\\t\\n'\n );\n },\n\n getSvgStyles: function(skipShadow) {\n var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow);\n return svgStyle + ' white-space: pre;';\n },\n\n /**\n * @private\n * @param {Number} textTopOffset Text top offset\n * @param {Number} textLeftOffset Text left offset\n * @return {Object}\n */\n _getSVGTextAndBg: function(textTopOffset, textLeftOffset) {\n var textSpans = [],\n textBgRects = [],\n height = 0;\n // bounding-box background\n this._setSVGBg(textBgRects);\n\n // text and text-background\n for (var i = 0, len = this._textLines.length; i < len; i++) {\n if (this.textBackgroundColor) {\n this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height);\n }\n this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects);\n height += this._getHeightOfLine(this.ctx, i);\n }\n\n return {\n textSpans: textSpans,\n textBgRects: textBgRects\n };\n },\n\n _setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) {\n var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction)\n - textTopOffset + height - this.height / 2;\n if (this.textAlign === 'justify') {\n // i call from here to do not intefere with IText\n this._setSVGTextLineJustifed(i, textSpans, yPos, textLeftOffset);\n return;\n }\n textSpans.push(\n '\\t\\t\\t elements since setting opacity\n // on containing one doesn't work in Illustrator\n this._getFillAttributes(this.fill), '>',\n fabric.util.string.escapeXml(this._textLines[i]),\n '\\n'\n );\n },\n\n _setSVGTextLineJustifed: function(i, textSpans, yPos, textLeftOffset) {\n var ctx = fabric.util.createCanvasElement().getContext('2d');\n\n this._setTextStyles(ctx);\n\n var line = this._textLines[i],\n words = line.split(/\\s+/),\n wordsWidth = this._getWidthOfWords(ctx, words.join('')),\n widthDiff = this.width - wordsWidth,\n numSpaces = words.length - 1,\n spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0,\n word, attributes = this._getFillAttributes(this.fill),\n len;\n\n textLeftOffset += this._getLineLeftOffset(this._getLineWidth(ctx, i));\n\n for (i = 0, len = words.length; i < len; i++) {\n word = words[i];\n textSpans.push(\n '\\t\\t\\t elements since setting opacity\n // on containing one doesn't work in Illustrator\n attributes, '>',\n fabric.util.string.escapeXml(word),\n '\\n'\n );\n textLeftOffset += this._getWidthOfWords(ctx, word) + spaceWidth;\n }\n },\n\n _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) {\n textBgRects.push(\n '\\t\\t\\n');\n },\n\n _setSVGBg: function(textBgRects) {\n if (this.backgroundColor) {\n textBgRects.push(\n '\\t\\t\\n');\n }\n },\n\n /**\n * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values\n * we work around it by \"moving\" alpha channel into opacity attribute and setting fill's alpha to 1\n *\n * @private\n * @param {*} value\n * @return {String}\n */\n _getFillAttributes: function(value) {\n var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : '';\n if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {\n return 'fill=\"' + value + '\"';\n }\n return 'opacity=\"' + fillColor.getAlpha() + '\" fill=\"' + fillColor.setAlpha(1).toRgb() + '\"';\n },\n /* _TO_SVG_END_ */\n\n /**\n * Sets specified property to a specified value\n * @param {String} key\n * @param {*} value\n * @return {fabric.Text} thisArg\n * @chainable\n */\n _set: function(key, value) {\n this.callSuper('_set', key, value);\n\n if (this._dimensionAffectingProps.indexOf(key) > -1) {\n this._initDimensions();\n this.setCoords();\n }\n },\n\n /**\n * Returns complexity of an instance\n * @return {Number} complexity\n */\n complexity: function() {\n return 1;\n }\n });\n\n /* _FROM_SVG_START_ */\n /**\n * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement})\n * @static\n * @memberOf fabric.Text\n * @see: http://www.w3.org/TR/SVG/text.html#TextElement\n */\n fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(\n 'x y dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' '));\n\n /**\n * Default SVG font size\n * @static\n * @memberOf fabric.Text\n */\n fabric.Text.DEFAULT_SVG_FONT_SIZE = 16;\n\n /**\n * Returns fabric.Text instance from an SVG element (not yet implemented)\n * @static\n * @memberOf fabric.Text\n * @param {SVGElement} element Element to parse\n * @param {Object} [options] Options object\n * @return {fabric.Text} Instance of fabric.Text\n */\n fabric.Text.fromElement = function(element, options) {\n if (!element) {\n return null;\n }\n\n var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES);\n options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes);\n\n options.top = options.top || 0;\n options.left = options.left || 0;\n if ('dx' in parsedAttributes) {\n options.left += parsedAttributes.dx;\n }\n if ('dy' in parsedAttributes) {\n options.top += parsedAttributes.dy;\n }\n if (!('fontSize' in options)) {\n options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;\n }\n\n if (!options.originX) {\n options.originX = 'left';\n }\n\n var textContent = '';\n\n // The XML is not properly parsed in IE9 so a workaround to get\n // textContent is through firstChild.data. Another workaround would be\n // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does)\n if (!('textContent' in element)) {\n if ('firstChild' in element && element.firstChild !== null) {\n if ('data' in element.firstChild && element.firstChild.data !== null) {\n textContent = element.firstChild.data;\n }\n }\n }\n else {\n textContent = element.textContent;\n }\n\n textContent = textContent.replace(/^\\s+|\\s+$|\\n+/g, '').replace(/\\s+/g, ' ');\n\n var text = new fabric.Text(textContent, options),\n textHeightScaleFactor = text.getHeight() / text.height,\n lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height,\n scaledDiff = lineHeightDiff * textHeightScaleFactor,\n textHeight = text.getHeight() + scaledDiff,\n offX = 0;\n /*\n Adjust positioning:\n x/y attributes in SVG correspond to the bottom-left corner of text bounding box\n top/left properties in Fabric correspond to center point of text bounding box\n */\n if (text.originX === 'left') {\n offX = text.getWidth() / 2;\n }\n if (text.originX === 'right') {\n offX = -text.getWidth() / 2;\n }\n text.set({\n left: text.getLeft() + offX,\n top: text.getTop() - textHeight / 2 + text.fontSize * (0.18 + text._fontSizeFraction) / text.lineHeight /* 0.3 is the old lineHeight */\n });\n\n return text;\n };\n /* _FROM_SVG_END_ */\n\n /**\n * Returns fabric.Text instance from an object representation\n * @static\n * @memberOf fabric.Text\n * @param {Object} object Object to create an instance from\n * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created\n * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first\n * @return {fabric.Text} Instance of fabric.Text\n */\n fabric.Text.fromObject = function(object, callback, forceAsync) {\n return fabric.Object._fromObject('Text', object, callback, forceAsync, 'text');\n };\n\n fabric.util.createAccessors(fabric.Text);\n\n})(typeof exports !== 'undefined' ? exports : this);\n\n\n(function() {\n\n var clone = fabric.util.object.clone;\n\n /**\n * IText class (introduced in v1.4) Events are also fired with \"text:\"\n * prefix when observing canvas.\n * @class fabric.IText\n * @extends fabric.Text\n * @mixes fabric.Observable\n *\n * @fires changed\n * @fires selection:changed\n * @fires editing:entered\n * @fires editing:exited\n *\n * @return {fabric.IText} thisArg\n * @see {@link fabric.IText#initialize} for constructor definition\n *\n * Supported key combinations:
\n * \n * Move cursor: left, right, up, down\n * Select character: shift + left, shift + right\n * Select text vertically: shift + up, shift + down\n * Move cursor by word: alt + left, alt + right\n * Select words: shift + alt + left, shift + alt + right\n * Move cursor to line start/end: cmd + left, cmd + right or home, end\n * Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end\n * Jump to start/end of text: cmd + up, cmd + down\n * Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown\n * Delete character: backspace\n * Delete word: alt + backspace\n * Delete line: cmd + backspace\n * Forward delete: delete\n * Copy text: ctrl/cmd + c\n * Paste text: ctrl/cmd + v\n * Cut text: ctrl/cmd + x\n * Select entire text: ctrl/cmd + a\n * Quit editing tab or esc\n *
\n *\n * Supported mouse/touch combination
\n * \n * Position cursor: click/touch\n * Create selection: click/touch & drag\n * Create selection: click & shift + click\n * Select word: double click\n * Select line: triple click\n *
\n */\n fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ {\n\n /**\n * Type of an object\n * @type String\n * @default\n */\n type: 'i-text',\n\n /**\n * Index where text selection starts (or where cursor is when there is no selection)\n * @type Number\n * @default\n */\n selectionStart: 0,\n\n /**\n * Index where text selection ends\n * @type Number\n * @default\n */\n selectionEnd: 0,\n\n /**\n * Color of text selection\n * @type String\n * @default\n */\n selectionColor: 'rgba(17,119,255,0.3)',\n\n /**\n * Indicates whether text is in editing mode\n * @type Boolean\n * @default\n */\n isEditing: false,\n\n /**\n * Indicates whether a text can be edited\n * @type Boolean\n * @default\n */\n editable: true,\n\n /**\n * Border color of text object while it's in editing mode\n * @type String\n * @default\n */\n editingBorderColor: 'rgba(102,153,255,0.25)',\n\n /**\n * Width of cursor (in px)\n * @type Number\n * @default\n */\n cursorWidth: 2,\n\n /**\n * Color of default cursor (when not overwritten by character style)\n * @type String\n * @default\n */\n cursorColor: '#333',\n\n /**\n * Delay between cursor blink (in ms)\n * @type Number\n * @default\n */\n cursorDelay: 1000,\n\n /**\n * Duration of cursor fadein (in ms)\n * @type Number\n * @default\n */\n cursorDuration: 600,\n\n /**\n * Object containing character styles\n * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line)\n * @type Object\n * @default\n */\n styles: null,\n\n /**\n * Indicates whether internal text char widths can be cached\n * @type Boolean\n * @default\n */\n caching: true,\n\n /**\n * @private\n */\n _reSpace: /\\s|\\n/,\n\n /**\n * @private\n */\n _currentCursorOpacity: 0,\n\n /**\n * @private\n */\n _selectionDirection: null,\n\n /**\n * @private\n */\n _abortCursorAnimation: false,\n\n /**\n * @private\n */\n __widthOfSpace: [],\n\n /**\n * Constructor\n * @param {String} text Text string\n * @param {Object} [options] Options object\n * @return {fabric.IText} thisArg\n */\n initialize: function(text, options) {\n this.styles = options ? (options.styles || { }) : { };\n this.callSuper('initialize', text, options);\n this.initBehavior();\n },\n\n /**\n * @private\n */\n _clearCache: function() {\n this.callSuper('_clearCache');\n this.__widthOfSpace = [];\n },\n\n /**\n * Returns true if object has no styling\n */\n isEmptyStyles: function() {\n if (!this.styles) {\n return true;\n }\n var obj = this.styles;\n\n for (var p1 in obj) {\n for (var p2 in obj[p1]) {\n // eslint-disable-next-line no-unused-vars\n for (var p3 in obj[p1][p2]) {\n return false;\n }\n }\n }\n return true;\n },\n\n /**\n * Sets selection start (left boundary of a selection)\n * @param {Number} index Index to set selection start to\n */\n setSelectionStart: function(index) {\n index = Math.max(index, 0);\n this._updateAndFire('selectionStart', index);\n },\n\n /**\n * Sets selection end (right boundary of a selection)\n * @param {Number} index Index to set selection end to\n */\n setSelectionEnd: function(index) {\n index = Math.min(index, this.text.length);\n this._updateAndFire('selectionEnd', index);\n },\n\n /**\n * @private\n * @param {String} property 'selectionStart' or 'selectionEnd'\n * @param {Number} index new position of property\n */\n _updateAndFire: function(property, index) {\n if (this[property] !== index) {\n this._fireSelectionChanged();\n this[property] = index;\n }\n this._updateTextarea();\n },\n\n /**\n * Fires the even of selection changed\n * @private\n */\n _fireSelectionChanged: function() {\n this.fire('selection:changed');\n this.canvas && this.canvas.fire('text:selection:changed', { target: this });\n },\n\n /**\n * Gets style of a current selection/cursor (at the start position)\n * @param {Number} [startIndex] Start index to get styles at\n * @param {Number} [endIndex] End index to get styles at\n * @return {Object} styles Style object at a specified (or current) index\n */\n getSelectionStyles: function(startIndex, endIndex) {\n\n if (arguments.length === 2) {\n var styles = [];\n for (var i = startIndex; i < endIndex; i++) {\n styles.push(this.getSelectionStyles(i));\n }\n return styles;\n }\n\n var loc = this.get2DCursorLocation(startIndex),\n style = this._getStyleDeclaration(loc.lineIndex, loc.charIndex);\n\n return style || {};\n },\n\n /**\n * Sets style of a current selection\n * @param {Object} [styles] Styles object\n * @return {fabric.IText} thisArg\n * @chainable\n */\n setSelectionStyles: function(styles) {\n if (this.selectionStart === this.selectionEnd) {\n this._extendStyles(this.selectionStart, styles);\n }\n else {\n for (var i = this.selectionStart; i < this.selectionEnd; i++) {\n this._extendStyles(i, styles);\n }\n }\n /* not included in _extendStyles to avoid clearing cache more than once */\n this._forceClearCache = true;\n return this;\n },\n\n /**\n * @private\n */\n _extendStyles: function(index, styles) {\n var loc = this.get2DCursorLocation(index);\n\n if (!this._getLineStyle(loc.lineIndex)) {\n this._setLineStyle(loc.lineIndex, {});\n }\n\n if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) {\n this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {});\n }\n\n fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles);\n },\n\n /**\n * Initialize text dimensions. Render all text on given context\n * or on a offscreen canvas to get the text width with measureText.\n * Updates this.width and this.height with the proper values.\n * Does not return dimensions.\n * @param {CanvasRenderingContext2D} [ctx] Context to render on\n * @private\n */\n _initDimensions: function(ctx) {\n if (!ctx) {\n this.clearContextTop();\n }\n this.callSuper('_initDimensions', ctx);\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Boolean} noTransform\n */\n render: function(ctx, noTransform) {\n this.clearContextTop();\n this.callSuper('render', ctx, noTransform);\n // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor\n // the correct position but not at every cursor animation.\n this.cursorOffsetCache = { };\n this.renderCursorOrSelection();\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _render: function(ctx) {\n this.callSuper('_render', ctx);\n this.ctx = ctx;\n },\n\n /**\n * Prepare and clean the contextTop\n */\n clearContextTop: function() {\n if (!this.active || !this.isEditing) {\n return;\n }\n if (this.canvas && this.canvas.contextTop) {\n var ctx = this.canvas.contextTop;\n ctx.save();\n ctx.transform.apply(ctx, this.canvas.viewportTransform);\n this.transform(ctx);\n this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);\n this._clearTextArea(ctx);\n ctx.restore();\n }\n },\n\n /**\n * Renders cursor or selection (depending on what exists)\n */\n renderCursorOrSelection: function() {\n if (!this.active || !this.isEditing) {\n return;\n }\n var chars = this.text.split(''),\n boundaries, ctx;\n if (this.canvas && this.canvas.contextTop) {\n ctx = this.canvas.contextTop;\n ctx.save();\n ctx.transform.apply(ctx, this.canvas.viewportTransform);\n this.transform(ctx);\n this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);\n this._clearTextArea(ctx);\n }\n else {\n ctx = this.ctx;\n ctx.save();\n }\n if (this.selectionStart === this.selectionEnd) {\n boundaries = this._getCursorBoundaries(chars, 'cursor');\n this.renderCursor(boundaries, ctx);\n }\n else {\n boundaries = this._getCursorBoundaries(chars, 'selection');\n this.renderSelection(chars, boundaries, ctx);\n }\n ctx.restore();\n },\n\n _clearTextArea: function(ctx) {\n // we add 4 pixel, to be sure to do not leave any pixel out\n var width = this.width + 4, height = this.height + 4;\n ctx.clearRect(-width / 2, -height / 2, width, height);\n },\n /**\n * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start)\n * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.\n */\n get2DCursorLocation: function(selectionStart) {\n if (typeof selectionStart === 'undefined') {\n selectionStart = this.selectionStart;\n }\n var len = this._textLines.length;\n for (var i = 0; i < len; i++) {\n if (selectionStart <= this._textLines[i].length) {\n return {\n lineIndex: i,\n charIndex: selectionStart\n };\n }\n selectionStart -= this._textLines[i].length + 1;\n }\n return {\n lineIndex: i - 1,\n charIndex: this._textLines[i - 1].length < selectionStart ? this._textLines[i - 1].length : selectionStart\n };\n },\n\n /**\n * Returns complete style of char at the current cursor\n * @param {Number} lineIndex Line index\n * @param {Number} charIndex Char index\n * @return {Object} Character style\n */\n getCurrentCharStyle: function(lineIndex, charIndex) {\n var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1);\n\n return {\n fontSize: style && style.fontSize || this.fontSize,\n fill: style && style.fill || this.fill,\n textBackgroundColor: style && style.textBackgroundColor || this.textBackgroundColor,\n textDecoration: style && style.textDecoration || this.textDecoration,\n fontFamily: style && style.fontFamily || this.fontFamily,\n fontWeight: style && style.fontWeight || this.fontWeight,\n fontStyle: style && style.fontStyle || this.fontStyle,\n stroke: style && style.stroke || this.stroke,\n strokeWidth: style && style.strokeWidth || this.strokeWidth\n };\n },\n\n /**\n * Returns fontSize of char at the current cursor\n * @param {Number} lineIndex Line index\n * @param {Number} charIndex Char index\n * @return {Number} Character font size\n */\n getCurrentCharFontSize: function(lineIndex, charIndex) {\n var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1);\n return style && style.fontSize ? style.fontSize : this.fontSize;\n },\n\n /**\n * Returns color (fill) of char at the current cursor\n * @param {Number} lineIndex Line index\n * @param {Number} charIndex Char index\n * @return {String} Character color (fill)\n */\n getCurrentCharColor: function(lineIndex, charIndex) {\n var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1);\n return style && style.fill ? style.fill : this.cursorColor;\n },\n\n /**\n * Returns cursor boundaries (left, top, leftOffset, topOffset)\n * @private\n * @param {Array} chars Array of characters\n * @param {String} typeOfBoundaries\n */\n _getCursorBoundaries: function(chars, typeOfBoundaries) {\n\n // left/top are left/top of entire text box\n // leftOffset/topOffset are offset from that left/top point of a text box\n\n var left = Math.round(this._getLeftOffset()),\n top = this._getTopOffset(),\n\n offsets = this._getCursorBoundariesOffsets(\n chars, typeOfBoundaries);\n\n return {\n left: left,\n top: top,\n leftOffset: offsets.left + offsets.lineLeft,\n topOffset: offsets.top\n };\n },\n\n /**\n * @private\n */\n _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) {\n if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {\n return this.cursorOffsetCache;\n }\n var lineLeftOffset = 0,\n lineIndex = 0,\n charIndex = 0,\n topOffset = 0,\n leftOffset = 0,\n boundaries;\n\n for (var i = 0; i < this.selectionStart; i++) {\n if (chars[i] === '\\n') {\n leftOffset = 0;\n topOffset += this._getHeightOfLine(this.ctx, lineIndex);\n\n lineIndex++;\n charIndex = 0;\n }\n else {\n leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex);\n charIndex++;\n }\n\n lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex));\n }\n if (typeOfBoundaries === 'cursor') {\n topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, lineIndex) / this.lineHeight\n - this.getCurrentCharFontSize(lineIndex, charIndex) * (1 - this._fontSizeFraction);\n }\n if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {\n leftOffset -= this._getWidthOfCharSpacing();\n }\n boundaries = {\n top: topOffset,\n left: leftOffset > 0 ? leftOffset : 0,\n lineLeft: lineLeftOffset\n };\n this.cursorOffsetCache = boundaries;\n return this.cursorOffsetCache;\n },\n\n /**\n * Renders cursor\n * @param {Object} boundaries\n * @param {CanvasRenderingContext2D} ctx transformed context to draw on\n */\n renderCursor: function(boundaries, ctx) {\n\n var cursorLocation = this.get2DCursorLocation(),\n lineIndex = cursorLocation.lineIndex,\n charIndex = cursorLocation.charIndex,\n charHeight = this.getCurrentCharFontSize(lineIndex, charIndex),\n leftOffset = boundaries.leftOffset,\n multiplier = this.scaleX * this.canvas.getZoom(),\n cursorWidth = this.cursorWidth / multiplier;\n\n ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex);\n ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;\n\n ctx.fillRect(\n boundaries.left + leftOffset - cursorWidth / 2,\n boundaries.top + boundaries.topOffset,\n cursorWidth,\n charHeight);\n },\n\n /**\n * Renders text selection\n * @param {Array} chars Array of characters\n * @param {Object} boundaries Object with left/top/leftOffset/topOffset\n * @param {CanvasRenderingContext2D} ctx transformed context to draw on\n */\n renderSelection: function(chars, boundaries, ctx) {\n\n ctx.fillStyle = this.selectionColor;\n\n var start = this.get2DCursorLocation(this.selectionStart),\n end = this.get2DCursorLocation(this.selectionEnd),\n startLine = start.lineIndex,\n endLine = end.lineIndex;\n for (var i = startLine; i <= endLine; i++) {\n var lineOffset = this._getLineLeftOffset(this._getLineWidth(ctx, i)) || 0,\n lineHeight = this._getHeightOfLine(this.ctx, i),\n realLineHeight = 0, boxWidth = 0, line = this._textLines[i];\n\n if (i === startLine) {\n for (var j = 0, len = line.length; j < len; j++) {\n if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) {\n boxWidth += this._getWidthOfChar(ctx, line[j], i, j);\n }\n if (j < start.charIndex) {\n lineOffset += this._getWidthOfChar(ctx, line[j], i, j);\n }\n }\n if (j === line.length) {\n boxWidth -= this._getWidthOfCharSpacing();\n }\n }\n else if (i > startLine && i < endLine) {\n boxWidth += this._getLineWidth(ctx, i) || 5;\n }\n else if (i === endLine) {\n for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) {\n boxWidth += this._getWidthOfChar(ctx, line[j2], i, j2);\n }\n if (end.charIndex === line.length) {\n boxWidth -= this._getWidthOfCharSpacing();\n }\n }\n realLineHeight = lineHeight;\n if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {\n lineHeight /= this.lineHeight;\n }\n ctx.fillRect(\n boundaries.left + lineOffset,\n boundaries.top + boundaries.topOffset,\n boxWidth > 0 ? boxWidth : 0,\n lineHeight);\n\n boundaries.topOffset += realLineHeight;\n }\n },\n\n /**\n * @private\n * @param {String} method\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} line Content of the line\n * @param {Number} left\n * @param {Number} top\n * @param {Number} lineIndex\n * @param {Number} charOffset\n */\n _renderChars: function(method, ctx, line, left, top, lineIndex, charOffset) {\n\n if (this.isEmptyStyles()) {\n return this._renderCharsFast(method, ctx, line, left, top);\n }\n\n charOffset = charOffset || 0;\n\n // set proper line offset\n var lineHeight = this._getHeightOfLine(ctx, lineIndex),\n prevStyle,\n thisStyle,\n charsToRender = '';\n\n ctx.save();\n top -= lineHeight / this.lineHeight * this._fontSizeFraction;\n for (var i = charOffset, len = line.length + charOffset; i <= len; i++) {\n prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i);\n thisStyle = this.getCurrentCharStyle(lineIndex, i + 1);\n\n if (this._hasStyleChanged(prevStyle, thisStyle) || i === len) {\n this._renderChar(method, ctx, lineIndex, i - 1, charsToRender, left, top, lineHeight);\n charsToRender = '';\n prevStyle = thisStyle;\n }\n charsToRender += line[i - charOffset];\n }\n ctx.restore();\n },\n\n /**\n * @private\n * @param {String} method\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} line Content of the line\n * @param {Number} left Left coordinate\n * @param {Number} top Top coordinate\n */\n _renderCharsFast: function(method, ctx, line, left, top) {\n\n if (method === 'fillText' && this.fill) {\n this.callSuper('_renderChars', method, ctx, line, left, top);\n }\n if (method === 'strokeText' && ((this.stroke && this.strokeWidth > 0) || this.skipFillStrokeCheck)) {\n this.callSuper('_renderChars', method, ctx, line, left, top);\n }\n },\n\n /**\n * @private\n * @param {String} method\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex\n * @param {Number} i\n * @param {String} _char\n * @param {Number} left Left coordinate\n * @param {Number} top Top coordinate\n * @param {Number} lineHeight Height of the line\n */\n _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) {\n var charWidth, charHeight, shouldFill, shouldStroke,\n decl = this._getStyleDeclaration(lineIndex, i),\n offset, textDecoration, chars, additionalSpace, _charWidth;\n\n if (decl) {\n charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i);\n shouldStroke = decl.stroke;\n shouldFill = decl.fill;\n textDecoration = decl.textDecoration;\n }\n else {\n charHeight = this.fontSize;\n }\n\n shouldStroke = (shouldStroke || this.stroke) && method === 'strokeText';\n shouldFill = (shouldFill || this.fill) && method === 'fillText';\n\n decl && ctx.save();\n\n charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || null);\n textDecoration = textDecoration || this.textDecoration;\n\n if (decl && decl.textBackgroundColor) {\n this._removeShadow(ctx);\n }\n if (this.charSpacing !== 0) {\n additionalSpace = this._getWidthOfCharSpacing();\n chars = _char.split('');\n charWidth = 0;\n for (var j = 0, len = chars.length, jChar; j < len; j++) {\n jChar = chars[j];\n shouldFill && ctx.fillText(jChar, left + charWidth, top);\n shouldStroke && ctx.strokeText(jChar, left + charWidth, top);\n _charWidth = ctx.measureText(jChar).width + additionalSpace;\n charWidth += _charWidth > 0 ? _charWidth : 0;\n }\n }\n else {\n shouldFill && ctx.fillText(_char, left, top);\n shouldStroke && ctx.strokeText(_char, left, top);\n }\n\n if (textDecoration || textDecoration !== '') {\n offset = this._fontSizeFraction * lineHeight / this.lineHeight;\n this._renderCharDecoration(ctx, textDecoration, left, top, offset, charWidth, charHeight);\n }\n\n decl && ctx.restore();\n ctx.translate(charWidth, 0);\n },\n\n /**\n * @private\n * @param {Object} prevStyle\n * @param {Object} thisStyle\n */\n _hasStyleChanged: function(prevStyle, thisStyle) {\n return (prevStyle.fill !== thisStyle.fill ||\n prevStyle.fontSize !== thisStyle.fontSize ||\n prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor ||\n prevStyle.textDecoration !== thisStyle.textDecoration ||\n prevStyle.fontFamily !== thisStyle.fontFamily ||\n prevStyle.fontWeight !== thisStyle.fontWeight ||\n prevStyle.fontStyle !== thisStyle.fontStyle ||\n prevStyle.stroke !== thisStyle.stroke ||\n prevStyle.strokeWidth !== thisStyle.strokeWidth\n );\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderCharDecoration: function(ctx, textDecoration, left, top, offset, charWidth, charHeight) {\n\n if (!textDecoration) {\n return;\n }\n\n var decorationWeight = charHeight / 15,\n positions = {\n underline: top + charHeight / 10,\n 'line-through': top - charHeight * (this._fontSizeFraction + this._fontSizeMult - 1) + decorationWeight,\n overline: top - (this._fontSizeMult - this._fontSizeFraction) * charHeight\n },\n decorations = ['underline', 'line-through', 'overline'], i, decoration;\n\n for (i = 0; i < decorations.length; i++) {\n decoration = decorations[i];\n if (textDecoration.indexOf(decoration) > -1) {\n ctx.fillRect(left, positions[decoration], charWidth , decorationWeight);\n }\n }\n },\n\n /**\n * @private\n * @param {String} method\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} line\n * @param {Number} left\n * @param {Number} top\n * @param {Number} lineIndex\n */\n _renderTextLine: function(method, ctx, line, left, top, lineIndex) {\n // to \"cancel\" this.fontSize subtraction in fabric.Text#_renderTextLine\n // the adding 0.03 is just to align text with itext by overlap test\n if (!this.isEmptyStyles()) {\n top += this.fontSize * (this._fontSizeFraction + 0.03);\n }\n this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex);\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderTextDecoration: function(ctx) {\n if (this.isEmptyStyles()) {\n return this.callSuper('_renderTextDecoration', ctx);\n }\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderTextLinesBackground: function(ctx) {\n this.callSuper('_renderTextLinesBackground', ctx);\n\n var lineTopOffset = 0, heightOfLine,\n lineWidth, lineLeftOffset,\n leftOffset = this._getLeftOffset(),\n topOffset = this._getTopOffset(),\n colorCache = '',\n line, _char, style, leftCache,\n topCache, widthCache, heightCache;\n ctx.save();\n for (var i = 0, len = this._textLines.length; i < len; i++) {\n heightOfLine = this._getHeightOfLine(ctx, i);\n line = this._textLines[i];\n\n if (line === '' || !this.styles || !this._getLineStyle(i)) {\n lineTopOffset += heightOfLine;\n continue;\n }\n\n lineWidth = this._getLineWidth(ctx, i);\n lineLeftOffset = this._getLineLeftOffset(lineWidth);\n leftCache = topCache = widthCache = heightCache = 0;\n for (var j = 0, jlen = line.length; j < jlen; j++) {\n style = this._getStyleDeclaration(i, j) || {};\n\n if (colorCache !== style.textBackgroundColor) {\n if (heightCache && widthCache) {\n ctx.fillStyle = colorCache;\n ctx.fillRect(leftCache, topCache, widthCache, heightCache);\n }\n leftCache = topCache = widthCache = heightCache = 0;\n colorCache = style.textBackgroundColor || '';\n }\n\n if (!style.textBackgroundColor) {\n colorCache = '';\n continue;\n }\n _char = line[j];\n\n if (colorCache === style.textBackgroundColor) {\n colorCache = style.textBackgroundColor;\n if (!leftCache) {\n leftCache = leftOffset + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j);\n }\n topCache = topOffset + lineTopOffset;\n widthCache += this._getWidthOfChar(ctx, _char, i, j);\n heightCache = heightOfLine / this.lineHeight;\n }\n }\n // if a textBackgroundColor ends on the last character of a line\n if (heightCache && widthCache) {\n ctx.fillStyle = colorCache;\n ctx.fillRect(leftCache, topCache, widthCache, heightCache);\n leftCache = topCache = widthCache = heightCache = 0;\n }\n lineTopOffset += heightOfLine;\n }\n ctx.restore();\n },\n\n /**\n * @private\n */\n _getCacheProp: function(_char, styleDeclaration) {\n return _char +\n styleDeclaration.fontSize +\n styleDeclaration.fontWeight +\n styleDeclaration.fontStyle;\n },\n\n /**\n * @private\n * @param {String} fontFamily name\n * @return {Object} reference to cache\n */\n _getFontCache: function(fontFamily) {\n if (!fabric.charWidthsCache[fontFamily]) {\n fabric.charWidthsCache[fontFamily] = { };\n }\n return fabric.charWidthsCache[fontFamily];\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} _char\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @param {Object} [decl]\n */\n _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) {\n var charDecl = decl || this._getStyleDeclaration(lineIndex, charIndex),\n styleDeclaration = clone(charDecl),\n width, cacheProp, charWidthsCache;\n\n this._applyFontStyles(styleDeclaration);\n charWidthsCache = this._getFontCache(styleDeclaration.fontFamily);\n cacheProp = this._getCacheProp(_char, styleDeclaration);\n\n // short-circuit if no styles for this char\n // global style from object is always applyed and handled by save and restore\n if (!charDecl && charWidthsCache[cacheProp] && this.caching) {\n return charWidthsCache[cacheProp];\n }\n\n if (typeof styleDeclaration.shadow === 'string') {\n styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow);\n }\n\n var fill = styleDeclaration.fill || this.fill;\n ctx.fillStyle = fill.toLive\n ? fill.toLive(ctx, this)\n : fill;\n\n if (styleDeclaration.stroke) {\n ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive)\n ? styleDeclaration.stroke.toLive(ctx, this)\n : styleDeclaration.stroke;\n }\n\n ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth;\n ctx.font = this._getFontDeclaration.call(styleDeclaration);\n\n //if we want this._setShadow.call to work with styleDeclarion\n //we have to add those references\n if (styleDeclaration.shadow) {\n styleDeclaration.scaleX = this.scaleX;\n styleDeclaration.scaleY = this.scaleY;\n styleDeclaration.canvas = this.canvas;\n styleDeclaration.getObjectScaling = this.getObjectScaling;\n this._setShadow.call(styleDeclaration, ctx);\n }\n\n if (!this.caching || !charWidthsCache[cacheProp]) {\n width = ctx.measureText(_char).width;\n this.caching && (charWidthsCache[cacheProp] = width);\n return width;\n }\n\n return charWidthsCache[cacheProp];\n },\n\n /**\n * @private\n * @param {Object} styleDeclaration\n */\n _applyFontStyles: function(styleDeclaration) {\n if (!styleDeclaration.fontFamily) {\n styleDeclaration.fontFamily = this.fontFamily;\n }\n if (!styleDeclaration.fontSize) {\n styleDeclaration.fontSize = this.fontSize;\n }\n if (!styleDeclaration.fontWeight) {\n styleDeclaration.fontWeight = this.fontWeight;\n }\n if (!styleDeclaration.fontStyle) {\n styleDeclaration.fontStyle = this.fontStyle;\n }\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @param {Boolean} [returnCloneOrEmpty=false]\n * @private\n */\n _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) {\n if (returnCloneOrEmpty) {\n return (this.styles[lineIndex] && this.styles[lineIndex][charIndex])\n ? clone(this.styles[lineIndex][charIndex])\n : { };\n }\n\n return this.styles[lineIndex] && this.styles[lineIndex][charIndex] ? this.styles[lineIndex][charIndex] : null;\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @param {Object} style\n * @private\n */\n _setStyleDeclaration: function(lineIndex, charIndex, style) {\n this.styles[lineIndex][charIndex] = style;\n },\n\n /**\n *\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @private\n */\n _deleteStyleDeclaration: function(lineIndex, charIndex) {\n delete this.styles[lineIndex][charIndex];\n },\n\n /**\n * @param {Number} lineIndex\n * @private\n */\n _getLineStyle: function(lineIndex) {\n return this.styles[lineIndex];\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Object} style\n * @private\n */\n _setLineStyle: function(lineIndex, style) {\n this.styles[lineIndex] = style;\n },\n\n /**\n * @param {Number} lineIndex\n * @private\n */\n _deleteLineStyle: function(lineIndex) {\n delete this.styles[lineIndex];\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) {\n if (!this._isMeasuring && this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) {\n return this._getWidthOfSpace(ctx, lineIndex);\n }\n ctx.save();\n var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex);\n if (this.charSpacing !== 0) {\n width += this._getWidthOfCharSpacing();\n }\n ctx.restore();\n return width > 0 ? width : 0;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex\n * @param {Number} charIndex\n */\n _getHeightOfChar: function(ctx, lineIndex, charIndex) {\n var style = this._getStyleDeclaration(lineIndex, charIndex);\n return style && style.fontSize ? style.fontSize : this.fontSize;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex\n * @param {Number} charIndex\n */\n _getWidthOfCharsAt: function(ctx, lineIndex, charIndex) {\n var width = 0, i, _char;\n for (i = 0; i < charIndex; i++) {\n _char = this._textLines[lineIndex][i];\n width += this._getWidthOfChar(ctx, _char, lineIndex, i);\n }\n return width;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex line number\n * @return {Number} Line width\n */\n _measureLine: function(ctx, lineIndex) {\n this._isMeasuring = true;\n var width = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length);\n if (this.charSpacing !== 0) {\n width -= this._getWidthOfCharSpacing();\n }\n this._isMeasuring = false;\n return width > 0 ? width : 0;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Number} lineIndex\n */\n _getWidthOfSpace: function (ctx, lineIndex) {\n if (this.__widthOfSpace[lineIndex]) {\n return this.__widthOfSpace[lineIndex];\n }\n var line = this._textLines[lineIndex],\n wordsWidth = this._getWidthOfWords(ctx, line, lineIndex, 0),\n widthDiff = this.width - wordsWidth,\n numSpaces = line.length - line.replace(this._reSpacesAndTabs, '').length,\n width = Math.max(widthDiff / numSpaces, ctx.measureText(' ').width);\n this.__widthOfSpace[lineIndex] = width;\n return width;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {String} line\n * @param {Number} lineIndex\n * @param {Number} charOffset\n */\n _getWidthOfWords: function (ctx, line, lineIndex, charOffset) {\n var width = 0;\n\n for (var charIndex = 0; charIndex < line.length; charIndex++) {\n var _char = line[charIndex];\n\n if (!_char.match(/\\s/)) {\n width += this._getWidthOfChar(ctx, _char, lineIndex, charIndex + charOffset);\n }\n }\n\n return width;\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _getHeightOfLine: function(ctx, lineIndex) {\n if (this.__lineHeights[lineIndex]) {\n return this.__lineHeights[lineIndex];\n }\n\n var line = this._textLines[lineIndex],\n maxHeight = this._getHeightOfChar(ctx, lineIndex, 0);\n\n for (var i = 1, len = line.length; i < len; i++) {\n var currentCharHeight = this._getHeightOfChar(ctx, lineIndex, i);\n if (currentCharHeight > maxHeight) {\n maxHeight = currentCharHeight;\n }\n }\n this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;\n return this.__lineHeights[lineIndex];\n },\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _getTextHeight: function(ctx) {\n var lineHeight, height = 0;\n for (var i = 0, len = this._textLines.length; i < len; i++) {\n lineHeight = this._getHeightOfLine(ctx, i);\n height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight);\n }\n return height;\n },\n\n /**\n * Returns object representation of an instance\n * @method toObject\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @return {Object} object representation of an instance\n */\n toObject: function(propertiesToInclude) {\n return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {\n styles: clone(this.styles, true)\n });\n }\n });\n\n /**\n * Returns fabric.IText instance from an object representation\n * @static\n * @memberOf fabric.IText\n * @param {Object} object Object to create an instance from\n * @param {function} [callback] invoked with new instance as argument\n * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first\n * @return {fabric.IText} instance of fabric.IText\n */\n fabric.IText.fromObject = function(object, callback, forceAsync) {\n return fabric.Object._fromObject('IText', object, callback, forceAsync, 'text');\n };\n})();\n\n\n(function() {\n\n var clone = fabric.util.object.clone;\n\n fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {\n\n /**\n * Initializes all the interactive behavior of IText\n */\n initBehavior: function() {\n this.initAddedHandler();\n this.initRemovedHandler();\n this.initCursorSelectionHandlers();\n this.initDoubleClickSimulation();\n this.mouseMoveHandler = this.mouseMoveHandler.bind(this);\n },\n\n onDeselect: function() {\n this.isEditing && this.exitEditing();\n this.selected = false;\n this.callSuper('onDeselect');\n },\n\n /**\n * Initializes \"added\" event handler\n */\n initAddedHandler: function() {\n var _this = this;\n this.on('added', function() {\n var canvas = _this.canvas;\n if (canvas) {\n if (!canvas._hasITextHandlers) {\n canvas._hasITextHandlers = true;\n _this._initCanvasHandlers(canvas);\n }\n canvas._iTextInstances = canvas._iTextInstances || [];\n canvas._iTextInstances.push(_this);\n }\n });\n },\n\n initRemovedHandler: function() {\n var _this = this;\n this.on('removed', function() {\n var canvas = _this.canvas;\n if (canvas) {\n canvas._iTextInstances = canvas._iTextInstances || [];\n fabric.util.removeFromArray(canvas._iTextInstances, _this);\n if (canvas._iTextInstances.length === 0) {\n canvas._hasITextHandlers = false;\n _this._removeCanvasHandlers(canvas);\n }\n }\n });\n },\n\n /**\n * register canvas event to manage exiting on other instances\n * @private\n */\n _initCanvasHandlers: function(canvas) {\n canvas._mouseUpITextHandler = (function() {\n if (canvas._iTextInstances) {\n canvas._iTextInstances.forEach(function(obj) {\n obj.__isMousedown = false;\n });\n }\n }).bind(this);\n canvas.on('mouse:up', canvas._mouseUpITextHandler);\n },\n\n /**\n * remove canvas event to manage exiting on other instances\n * @private\n */\n _removeCanvasHandlers: function(canvas) {\n canvas.off('mouse:up', canvas._mouseUpITextHandler);\n },\n\n /**\n * @private\n */\n _tick: function() {\n this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete');\n },\n\n /**\n * @private\n */\n _animateCursor: function(obj, targetOpacity, duration, completeMethod) {\n\n var tickState;\n\n tickState = {\n isAborted: false,\n abort: function() {\n this.isAborted = true;\n },\n };\n\n obj.animate('_currentCursorOpacity', targetOpacity, {\n duration: duration,\n onComplete: function() {\n if (!tickState.isAborted) {\n obj[completeMethod]();\n }\n },\n onChange: function() {\n // we do not want to animate a selection, only cursor\n if (obj.canvas && obj.selectionStart === obj.selectionEnd) {\n obj.renderCursorOrSelection();\n }\n },\n abort: function() {\n return tickState.isAborted;\n }\n });\n return tickState;\n },\n\n /**\n * @private\n */\n _onTickComplete: function() {\n\n var _this = this;\n\n if (this._cursorTimeout1) {\n clearTimeout(this._cursorTimeout1);\n }\n this._cursorTimeout1 = setTimeout(function() {\n _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick');\n }, 100);\n },\n\n /**\n * Initializes delayed cursor\n */\n initDelayedCursor: function(restart) {\n var _this = this,\n delay = restart ? 0 : this.cursorDelay;\n\n this.abortCursorAnimation();\n this._currentCursorOpacity = 1;\n this._cursorTimeout2 = setTimeout(function() {\n _this._tick();\n }, delay);\n },\n\n /**\n * Aborts cursor animation and clears all timeouts\n */\n abortCursorAnimation: function() {\n var shouldClear = this._currentTickState || this._currentTickCompleteState;\n this._currentTickState && this._currentTickState.abort();\n this._currentTickCompleteState && this._currentTickCompleteState.abort();\n\n clearTimeout(this._cursorTimeout1);\n clearTimeout(this._cursorTimeout2);\n\n this._currentCursorOpacity = 0;\n // to clear just itext area we need to transform the context\n // it may not be worth it\n if (shouldClear) {\n this.canvas && this.canvas.clearContext(this.canvas.contextTop || this.ctx);\n }\n\n },\n\n /**\n * Selects entire text\n */\n selectAll: function() {\n this.selectionStart = 0;\n this.selectionEnd = this.text.length;\n this._fireSelectionChanged();\n this._updateTextarea();\n },\n\n /**\n * Returns selected text\n * @return {String}\n */\n getSelectedText: function() {\n return this.text.slice(this.selectionStart, this.selectionEnd);\n },\n\n /**\n * Find new selection index representing start of current word according to current selection index\n * @param {Number} startFrom Surrent selection index\n * @return {Number} New selection index\n */\n findWordBoundaryLeft: function(startFrom) {\n var offset = 0, index = startFrom - 1;\n\n // remove space before cursor first\n if (this._reSpace.test(this.text.charAt(index))) {\n while (this._reSpace.test(this.text.charAt(index))) {\n offset++;\n index--;\n }\n }\n while (/\\S/.test(this.text.charAt(index)) && index > -1) {\n offset++;\n index--;\n }\n\n return startFrom - offset;\n },\n\n /**\n * Find new selection index representing end of current word according to current selection index\n * @param {Number} startFrom Current selection index\n * @return {Number} New selection index\n */\n findWordBoundaryRight: function(startFrom) {\n var offset = 0, index = startFrom;\n\n // remove space after cursor first\n if (this._reSpace.test(this.text.charAt(index))) {\n while (this._reSpace.test(this.text.charAt(index))) {\n offset++;\n index++;\n }\n }\n while (/\\S/.test(this.text.charAt(index)) && index < this.text.length) {\n offset++;\n index++;\n }\n\n return startFrom + offset;\n },\n\n /**\n * Find new selection index representing start of current line according to current selection index\n * @param {Number} startFrom Current selection index\n * @return {Number} New selection index\n */\n findLineBoundaryLeft: function(startFrom) {\n var offset = 0, index = startFrom - 1;\n\n while (!/\\n/.test(this.text.charAt(index)) && index > -1) {\n offset++;\n index--;\n }\n\n return startFrom - offset;\n },\n\n /**\n * Find new selection index representing end of current line according to current selection index\n * @param {Number} startFrom Current selection index\n * @return {Number} New selection index\n */\n findLineBoundaryRight: function(startFrom) {\n var offset = 0, index = startFrom;\n\n while (!/\\n/.test(this.text.charAt(index)) && index < this.text.length) {\n offset++;\n index++;\n }\n\n return startFrom + offset;\n },\n\n /**\n * Returns number of newlines in selected text\n * @return {Number} Number of newlines in selected text\n */\n getNumNewLinesInSelectedText: function() {\n var selectedText = this.getSelectedText(),\n numNewLines = 0;\n\n for (var i = 0, len = selectedText.length; i < len; i++) {\n if (selectedText[i] === '\\n') {\n numNewLines++;\n }\n }\n return numNewLines;\n },\n\n /**\n * Finds index corresponding to beginning or end of a word\n * @param {Number} selectionStart Index of a character\n * @param {Number} direction 1 or -1\n * @return {Number} Index of the beginning or end of a word\n */\n searchWordBoundary: function(selectionStart, direction) {\n var index = this._reSpace.test(this.text.charAt(selectionStart)) ? selectionStart - 1 : selectionStart,\n _char = this.text.charAt(index),\n reNonWord = /[ \\n\\.,;!\\?\\-]/;\n\n while (!reNonWord.test(_char) && index > 0 && index < this.text.length) {\n index += direction;\n _char = this.text.charAt(index);\n }\n if (reNonWord.test(_char) && _char !== '\\n') {\n index += direction === 1 ? 0 : 1;\n }\n return index;\n },\n\n /**\n * Selects a word based on the index\n * @param {Number} selectionStart Index of a character\n */\n selectWord: function(selectionStart) {\n selectionStart = selectionStart || this.selectionStart;\n var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */\n newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */\n\n this.selectionStart = newSelectionStart;\n this.selectionEnd = newSelectionEnd;\n this._fireSelectionChanged();\n this._updateTextarea();\n this.renderCursorOrSelection();\n },\n\n /**\n * Selects a line based on the index\n * @param {Number} selectionStart Index of a character\n */\n selectLine: function(selectionStart) {\n selectionStart = selectionStart || this.selectionStart;\n var newSelectionStart = this.findLineBoundaryLeft(selectionStart),\n newSelectionEnd = this.findLineBoundaryRight(selectionStart);\n\n this.selectionStart = newSelectionStart;\n this.selectionEnd = newSelectionEnd;\n this._fireSelectionChanged();\n this._updateTextarea();\n },\n\n /**\n * Enters editing state\n * @return {fabric.IText} thisArg\n * @chainable\n */\n enterEditing: function(e) {\n if (this.isEditing || !this.editable) {\n return;\n }\n\n if (this.canvas) {\n this.exitEditingOnOthers(this.canvas);\n }\n\n this.isEditing = true;\n this.selected = true;\n this.initHiddenTextarea(e);\n this.hiddenTextarea.focus();\n this._updateTextarea();\n this._saveEditingProps();\n this._setEditingProps();\n this._textBeforeEdit = this.text;\n\n this._tick();\n this.fire('editing:entered');\n this._fireSelectionChanged();\n if (!this.canvas) {\n return this;\n }\n this.canvas.fire('text:editing:entered', { target: this });\n this.initMouseMoveHandler();\n this.canvas.renderAll();\n return this;\n },\n\n exitEditingOnOthers: function(canvas) {\n if (canvas._iTextInstances) {\n canvas._iTextInstances.forEach(function(obj) {\n obj.selected = false;\n if (obj.isEditing) {\n obj.exitEditing();\n }\n });\n }\n },\n\n /**\n * Initializes \"mousemove\" event handler\n */\n initMouseMoveHandler: function() {\n this.canvas.on('mouse:move', this.mouseMoveHandler);\n },\n\n /**\n * @private\n */\n mouseMoveHandler: function(options) {\n if (!this.__isMousedown || !this.isEditing) {\n return;\n }\n\n var newSelectionStart = this.getSelectionStartFromPointer(options.e),\n currentStart = this.selectionStart,\n currentEnd = this.selectionEnd;\n if (\n (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd)\n &&\n (currentStart === newSelectionStart || currentEnd === newSelectionStart)\n ) {\n return;\n }\n if (newSelectionStart > this.__selectionStartOnMouseDown) {\n this.selectionStart = this.__selectionStartOnMouseDown;\n this.selectionEnd = newSelectionStart;\n }\n else {\n this.selectionStart = newSelectionStart;\n this.selectionEnd = this.__selectionStartOnMouseDown;\n }\n if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) {\n this.restartCursorIfNeeded();\n this._fireSelectionChanged();\n this._updateTextarea();\n this.renderCursorOrSelection();\n }\n },\n\n /**\n * @private\n */\n _setEditingProps: function() {\n this.hoverCursor = 'text';\n\n if (this.canvas) {\n this.canvas.defaultCursor = this.canvas.moveCursor = 'text';\n }\n\n this.borderColor = this.editingBorderColor;\n\n this.hasControls = this.selectable = false;\n this.lockMovementX = this.lockMovementY = true;\n },\n\n /**\n * @private\n */\n _updateTextarea: function() {\n if (!this.hiddenTextarea || this.inCompositionMode) {\n return;\n }\n this.cursorOffsetCache = { };\n this.hiddenTextarea.value = this.text;\n this.hiddenTextarea.selectionStart = this.selectionStart;\n this.hiddenTextarea.selectionEnd = this.selectionEnd;\n if (this.selectionStart === this.selectionEnd) {\n var style = this._calcTextareaPosition();\n this.hiddenTextarea.style.left = style.left;\n this.hiddenTextarea.style.top = style.top;\n this.hiddenTextarea.style.fontSize = style.fontSize;\n }\n },\n\n /**\n * @private\n * @return {Object} style contains style for hiddenTextarea\n */\n _calcTextareaPosition: function() {\n if (!this.canvas) {\n return { x: 1, y: 1 };\n }\n var chars = this.text.split(''),\n boundaries = this._getCursorBoundaries(chars, 'cursor'),\n cursorLocation = this.get2DCursorLocation(),\n lineIndex = cursorLocation.lineIndex,\n charIndex = cursorLocation.charIndex,\n charHeight = this.getCurrentCharFontSize(lineIndex, charIndex),\n leftOffset = boundaries.leftOffset,\n m = this.calcTransformMatrix(),\n p = {\n x: boundaries.left + leftOffset,\n y: boundaries.top + boundaries.topOffset + charHeight\n },\n upperCanvas = this.canvas.upperCanvasEl,\n maxWidth = upperCanvas.width - charHeight,\n maxHeight = upperCanvas.height - charHeight;\n\n p = fabric.util.transformPoint(p, m);\n p = fabric.util.transformPoint(p, this.canvas.viewportTransform);\n\n if (p.x < 0) {\n p.x = 0;\n }\n if (p.x > maxWidth) {\n p.x = maxWidth;\n }\n if (p.y < 0) {\n p.y = 0;\n }\n if (p.y > maxHeight) {\n p.y = maxHeight;\n }\n\n // add canvas offset on document\n p.x += this.canvas._offset.left;\n p.y += this.canvas._offset.top;\n\n return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight };\n },\n\n /**\n * @private\n */\n _saveEditingProps: function() {\n this._savedProps = {\n hasControls: this.hasControls,\n borderColor: this.borderColor,\n lockMovementX: this.lockMovementX,\n lockMovementY: this.lockMovementY,\n hoverCursor: this.hoverCursor,\n defaultCursor: this.canvas && this.canvas.defaultCursor,\n moveCursor: this.canvas && this.canvas.moveCursor\n };\n },\n\n /**\n * @private\n */\n _restoreEditingProps: function() {\n if (!this._savedProps) {\n return;\n }\n\n this.hoverCursor = this._savedProps.overCursor;\n this.hasControls = this._savedProps.hasControls;\n this.borderColor = this._savedProps.borderColor;\n this.lockMovementX = this._savedProps.lockMovementX;\n this.lockMovementY = this._savedProps.lockMovementY;\n\n if (this.canvas) {\n this.canvas.defaultCursor = this._savedProps.defaultCursor;\n this.canvas.moveCursor = this._savedProps.moveCursor;\n }\n },\n\n /**\n * Exits from editing state\n * @return {fabric.IText} thisArg\n * @chainable\n */\n exitEditing: function() {\n var isTextChanged = (this._textBeforeEdit !== this.text);\n this.selected = false;\n this.isEditing = false;\n this.selectable = true;\n\n this.selectionEnd = this.selectionStart;\n\n if (this.hiddenTextarea) {\n this.hiddenTextarea.blur && this.hiddenTextarea.blur();\n this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea);\n this.hiddenTextarea = null;\n }\n\n this.abortCursorAnimation();\n this._restoreEditingProps();\n this._currentCursorOpacity = 0;\n\n this.fire('editing:exited');\n isTextChanged && this.fire('modified');\n if (this.canvas) {\n this.canvas.off('mouse:move', this.mouseMoveHandler);\n this.canvas.fire('text:editing:exited', { target: this });\n isTextChanged && this.canvas.fire('object:modified', { target: this });\n }\n return this;\n },\n\n /**\n * @private\n */\n _removeExtraneousStyles: function() {\n for (var prop in this.styles) {\n if (!this._textLines[prop]) {\n delete this.styles[prop];\n }\n }\n },\n\n /**\n * @private\n */\n _removeCharsFromTo: function(start, end) {\n while (end !== start) {\n this._removeSingleCharAndStyle(start + 1);\n end--;\n }\n this.selectionStart = start;\n this.selectionEnd = start;\n },\n\n _removeSingleCharAndStyle: function(index) {\n var isBeginningOfLine = this.text[index - 1] === '\\n',\n indexStyle = isBeginningOfLine ? index : index - 1;\n this.removeStyleObject(isBeginningOfLine, indexStyle);\n this.text = this.text.slice(0, index - 1) +\n this.text.slice(index);\n\n this._textLines = this._splitTextIntoLines();\n },\n\n /**\n * Inserts characters where cursor is (replacing selection if one exists)\n * @param {String} _chars Characters to insert\n * @param {Boolean} useCopiedStyle use fabric.copiedTextStyle\n */\n insertChars: function(_chars, useCopiedStyle) {\n var style;\n\n if (this.selectionEnd - this.selectionStart > 1) {\n this._removeCharsFromTo(this.selectionStart, this.selectionEnd);\n }\n //short circuit for block paste\n if (!useCopiedStyle && this.isEmptyStyles()) {\n this.insertChar(_chars, false);\n return;\n }\n for (var i = 0, len = _chars.length; i < len; i++) {\n if (useCopiedStyle) {\n style = fabric.util.object.clone(fabric.copiedTextStyle[i], true);\n }\n this.insertChar(_chars[i], i < len - 1, style);\n }\n },\n\n /**\n * Inserts a character where cursor is\n * @param {String} _char Characters to insert\n * @param {Boolean} skipUpdate trigger rendering and updates at the end of text insert\n * @param {Object} styleObject Style to be inserted for the new char\n */\n insertChar: function(_char, skipUpdate, styleObject) {\n var isEndOfLine = this.text[this.selectionStart] === '\\n';\n this.text = this.text.slice(0, this.selectionStart) +\n _char + this.text.slice(this.selectionEnd);\n this._textLines = this._splitTextIntoLines();\n this.insertStyleObjects(_char, isEndOfLine, styleObject);\n this.selectionStart += _char.length;\n this.selectionEnd = this.selectionStart;\n if (skipUpdate) {\n return;\n }\n this._updateTextarea();\n this.setCoords();\n this._fireSelectionChanged();\n this.fire('changed');\n this.restartCursorIfNeeded();\n if (this.canvas) {\n this.canvas.fire('text:changed', { target: this });\n this.canvas.renderAll();\n }\n },\n\n restartCursorIfNeeded: function() {\n if (!this._currentTickState || this._currentTickState.isAborted\n || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted\n ) {\n this.initDelayedCursor();\n }\n },\n\n /**\n * Inserts new style object\n * @param {Number} lineIndex Index of a line\n * @param {Number} charIndex Index of a char\n * @param {Boolean} isEndOfLine True if it's end of line\n */\n insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) {\n\n this.shiftLineStyles(lineIndex, +1);\n\n var currentCharStyle = {},\n newLineStyles = {};\n\n if (this.styles[lineIndex] && this.styles[lineIndex][charIndex - 1]) {\n currentCharStyle = this.styles[lineIndex][charIndex - 1];\n }\n\n // if there's nothing after cursor,\n // we clone current char style onto the next (otherwise empty) line\n if (isEndOfLine && currentCharStyle) {\n newLineStyles[0] = clone(currentCharStyle);\n this.styles[lineIndex + 1] = newLineStyles;\n }\n // otherwise we clone styles of all chars\n // after cursor onto the next line, from the beginning\n else {\n var somethingAdded = false;\n for (var index in this.styles[lineIndex]) {\n var numIndex = parseInt(index, 10);\n if (numIndex >= charIndex) {\n somethingAdded = true;\n newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];\n // remove lines from the previous line since they're on a new line now\n delete this.styles[lineIndex][index];\n }\n }\n somethingAdded && (this.styles[lineIndex + 1] = newLineStyles);\n }\n this._forceClearCache = true;\n },\n\n /**\n * Inserts style object for a given line/char index\n * @param {Number} lineIndex Index of a line\n * @param {Number} charIndex Index of a char\n * @param {Object} [style] Style object to insert, if given\n */\n insertCharStyleObject: function(lineIndex, charIndex, style) {\n\n var currentLineStyles = this.styles[lineIndex],\n currentLineStylesCloned = clone(currentLineStyles);\n\n if (charIndex === 0 && !style) {\n charIndex = 1;\n }\n\n // shift all char styles by 1 forward\n // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4\n for (var index in currentLineStylesCloned) {\n var numericIndex = parseInt(index, 10);\n\n if (numericIndex >= charIndex) {\n currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex];\n\n // only delete the style if there was nothing moved there\n if (!currentLineStylesCloned[numericIndex - 1]) {\n delete currentLineStyles[numericIndex];\n }\n }\n }\n var newStyle = style || clone(currentLineStyles[charIndex - 1]);\n newStyle && (this.styles[lineIndex][charIndex] = newStyle);\n this._forceClearCache = true;\n },\n\n /**\n * Inserts style object(s)\n * @param {String} _chars Characters at the location where style is inserted\n * @param {Boolean} isEndOfLine True if it's end of line\n * @param {Object} [styleObject] Style to insert\n */\n insertStyleObjects: function(_chars, isEndOfLine, styleObject) {\n // removed shortcircuit over isEmptyStyles\n\n var cursorLocation = this.get2DCursorLocation(),\n lineIndex = cursorLocation.lineIndex,\n charIndex = cursorLocation.charIndex;\n\n if (!this._getLineStyle(lineIndex)) {\n this._setLineStyle(lineIndex, {});\n }\n\n if (_chars === '\\n') {\n this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine);\n }\n else {\n this.insertCharStyleObject(lineIndex, charIndex, styleObject);\n }\n },\n\n /**\n * Shifts line styles up or down\n * @param {Number} lineIndex Index of a line\n * @param {Number} offset Can be -1 or +1\n */\n shiftLineStyles: function(lineIndex, offset) {\n // shift all line styles by 1 upward or downward\n var clonedStyles = clone(this.styles);\n for (var line in clonedStyles) {\n var numericLine = parseInt(line, 10);\n if (numericLine <= lineIndex) {\n delete clonedStyles[numericLine];\n }\n }\n for (var line in this.styles) {\n var numericLine = parseInt(line, 10);\n if (numericLine > lineIndex) {\n this.styles[numericLine + offset] = clonedStyles[numericLine];\n if (!clonedStyles[numericLine - offset]) {\n delete this.styles[numericLine];\n }\n }\n }\n //TODO: evaluate if delete old style lines with offset -1\n },\n\n /**\n * Removes style object\n * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line\n * @param {Number} [index] Optional index. When not given, current selectionStart is used.\n */\n removeStyleObject: function(isBeginningOfLine, index) {\n\n var cursorLocation = this.get2DCursorLocation(index),\n lineIndex = cursorLocation.lineIndex,\n charIndex = cursorLocation.charIndex;\n\n this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex);\n },\n\n _getTextOnPreviousLine: function(lIndex) {\n return this._textLines[lIndex - 1];\n },\n\n _removeStyleObject: function(isBeginningOfLine, cursorLocation, lineIndex, charIndex) {\n\n if (isBeginningOfLine) {\n var textOnPreviousLine = this._getTextOnPreviousLine(cursorLocation.lineIndex),\n newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0;\n\n if (!this.styles[lineIndex - 1]) {\n this.styles[lineIndex - 1] = {};\n }\n for (charIndex in this.styles[lineIndex]) {\n this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine]\n = this.styles[lineIndex][charIndex];\n }\n this.shiftLineStyles(cursorLocation.lineIndex, -1);\n }\n else {\n var currentLineStyles = this.styles[lineIndex];\n\n if (currentLineStyles) {\n delete currentLineStyles[charIndex];\n }\n var currentLineStylesCloned = clone(currentLineStyles);\n // shift all styles by 1 backwards\n for (var i in currentLineStylesCloned) {\n var numericIndex = parseInt(i, 10);\n if (numericIndex >= charIndex && numericIndex !== 0) {\n currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex];\n delete currentLineStyles[numericIndex];\n }\n }\n }\n },\n\n /**\n * Inserts new line\n */\n insertNewline: function() {\n this.insertChars('\\n');\n },\n\n /**\n * Set the selectionStart and selectionEnd according to the ne postion of cursor\n * mimic the key - mouse navigation when shift is pressed.\n */\n setSelectionStartEndWithShift: function(start, end, newSelection) {\n if (newSelection <= start) {\n if (end === start) {\n this._selectionDirection = 'left';\n }\n else if (this._selectionDirection === 'right') {\n this._selectionDirection = 'left';\n this.selectionEnd = start;\n }\n this.selectionStart = newSelection;\n }\n else if (newSelection > start && newSelection < end) {\n if (this._selectionDirection === 'right') {\n this.selectionEnd = newSelection;\n }\n else {\n this.selectionStart = newSelection;\n }\n }\n else {\n // newSelection is > selection start and end\n if (end === start) {\n this._selectionDirection = 'right';\n }\n else if (this._selectionDirection === 'left') {\n this._selectionDirection = 'right';\n this.selectionStart = end;\n }\n this.selectionEnd = newSelection;\n }\n },\n\n setSelectionInBoundaries: function() {\n var length = this.text.length;\n if (this.selectionStart > length) {\n this.selectionStart = length;\n }\n else if (this.selectionStart < 0) {\n this.selectionStart = 0;\n }\n if (this.selectionEnd > length) {\n this.selectionEnd = length;\n }\n else if (this.selectionEnd < 0) {\n this.selectionEnd = 0;\n }\n }\n });\n})();\n\n\nfabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {\n /**\n * Initializes \"dbclick\" event handler\n */\n initDoubleClickSimulation: function() {\n\n // for double click\n this.__lastClickTime = +new Date();\n\n // for triple click\n this.__lastLastClickTime = +new Date();\n\n this.__lastPointer = { };\n\n this.on('mousedown', this.onMouseDown.bind(this));\n },\n\n onMouseDown: function(options) {\n\n this.__newClickTime = +new Date();\n var newPointer = this.canvas.getPointer(options.e);\n\n if (this.isTripleClick(newPointer, options.e)) {\n this.fire('tripleclick', options);\n this._stopEvent(options.e);\n }\n else if (this.isDoubleClick(newPointer)) {\n this.fire('dblclick', options);\n this._stopEvent(options.e);\n }\n\n this.__lastLastClickTime = this.__lastClickTime;\n this.__lastClickTime = this.__newClickTime;\n this.__lastPointer = newPointer;\n this.__lastIsEditing = this.isEditing;\n this.__lastSelected = this.selected;\n },\n\n isDoubleClick: function(newPointer) {\n return this.__newClickTime - this.__lastClickTime < 500 &&\n this.__lastPointer.x === newPointer.x &&\n this.__lastPointer.y === newPointer.y && this.__lastIsEditing;\n },\n\n isTripleClick: function(newPointer) {\n return this.__newClickTime - this.__lastClickTime < 500 &&\n this.__lastClickTime - this.__lastLastClickTime < 500 &&\n this.__lastPointer.x === newPointer.x &&\n this.__lastPointer.y === newPointer.y;\n },\n\n /**\n * @private\n */\n _stopEvent: function(e) {\n e.preventDefault && e.preventDefault();\n e.stopPropagation && e.stopPropagation();\n },\n\n /**\n * Initializes event handlers related to cursor or selection\n */\n initCursorSelectionHandlers: function() {\n this.initMousedownHandler();\n this.initMouseupHandler();\n this.initClicks();\n },\n\n /**\n * Initializes double and triple click event handlers\n */\n initClicks: function() {\n this.on('dblclick', function(options) {\n this.selectWord(this.getSelectionStartFromPointer(options.e));\n });\n this.on('tripleclick', function(options) {\n this.selectLine(this.getSelectionStartFromPointer(options.e));\n });\n },\n\n /**\n * Initializes \"mousedown\" event handler\n */\n initMousedownHandler: function() {\n this.on('mousedown', function(options) {\n if (!this.editable || (options.e.button && options.e.button !== 1)) {\n return;\n }\n var pointer = this.canvas.getPointer(options.e);\n this.__mousedownX = pointer.x;\n this.__mousedownY = pointer.y;\n this.__isMousedown = true;\n\n if (this.selected) {\n this.setCursorByClick(options.e);\n }\n\n if (this.isEditing) {\n this.__selectionStartOnMouseDown = this.selectionStart;\n if (this.selectionStart === this.selectionEnd) {\n this.abortCursorAnimation();\n }\n this.renderCursorOrSelection();\n }\n });\n },\n\n /**\n * @private\n */\n _isObjectMoved: function(e) {\n var pointer = this.canvas.getPointer(e);\n\n return this.__mousedownX !== pointer.x ||\n this.__mousedownY !== pointer.y;\n },\n\n /**\n * Initializes \"mouseup\" event handler\n */\n initMouseupHandler: function() {\n this.on('mouseup', function(options) {\n this.__isMousedown = false;\n if (!this.editable || this._isObjectMoved(options.e) || (options.e.button && options.e.button !== 1)) {\n return;\n }\n\n if (this.__lastSelected && !this.__corner) {\n this.enterEditing(options.e);\n if (this.selectionStart === this.selectionEnd) {\n this.initDelayedCursor(true);\n }\n else {\n this.renderCursorOrSelection();\n }\n }\n this.selected = true;\n });\n },\n\n /**\n * Changes cursor location in a text depending on passed pointer (x/y) object\n * @param {Event} e Event object\n */\n setCursorByClick: function(e) {\n var newSelection = this.getSelectionStartFromPointer(e),\n start = this.selectionStart, end = this.selectionEnd;\n if (e.shiftKey) {\n this.setSelectionStartEndWithShift(start, end, newSelection);\n }\n else {\n this.selectionStart = newSelection;\n this.selectionEnd = newSelection;\n }\n if (this.isEditing) {\n this._fireSelectionChanged();\n this._updateTextarea();\n }\n },\n\n /**\n * Returns index of a character corresponding to where an object was clicked\n * @param {Event} e Event object\n * @return {Number} Index of a character\n */\n getSelectionStartFromPointer: function(e) {\n var mouseOffset = this.getLocalPointer(e),\n prevWidth = 0,\n width = 0,\n height = 0,\n charIndex = 0,\n newSelectionStart,\n line;\n\n for (var i = 0, len = this._textLines.length; i < len; i++) {\n line = this._textLines[i];\n height += this._getHeightOfLine(this.ctx, i) * this.scaleY;\n\n var widthOfLine = this._getLineWidth(this.ctx, i),\n lineLeftOffset = this._getLineLeftOffset(widthOfLine);\n\n width = lineLeftOffset * this.scaleX;\n\n for (var j = 0, jlen = line.length; j < jlen; j++) {\n\n prevWidth = width;\n\n width += this._getWidthOfChar(this.ctx, line[j], i, this.flipX ? jlen - j : j) *\n this.scaleX;\n\n if (height <= mouseOffset.y || width <= mouseOffset.x) {\n charIndex++;\n continue;\n }\n\n return this._getNewSelectionStartFromOffset(\n mouseOffset, prevWidth, width, charIndex + i, jlen);\n }\n\n if (mouseOffset.y < height) {\n //this happens just on end of lines.\n return this._getNewSelectionStartFromOffset(\n mouseOffset, prevWidth, width, charIndex + i - 1, jlen);\n }\n }\n\n // clicked somewhere after all chars, so set at the end\n if (typeof newSelectionStart === 'undefined') {\n return this.text.length;\n }\n },\n\n /**\n * @private\n */\n _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {\n\n var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,\n distanceBtwNextCharAndCursor = width - mouseOffset.x,\n offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1,\n newSelectionStart = index + offset;\n\n // if object is horizontally flipped, mirror cursor location from the end\n if (this.flipX) {\n newSelectionStart = jlen - newSelectionStart;\n }\n\n if (newSelectionStart > this.text.length) {\n newSelectionStart = this.text.length;\n }\n\n return newSelectionStart;\n }\n});\n\n\nfabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {\n\n /**\n * Initializes hidden textarea (needed to bring up keyboard in iOS)\n */\n initHiddenTextarea: function() {\n this.hiddenTextarea = fabric.document.createElement('textarea');\n this.hiddenTextarea.setAttribute('autocapitalize', 'off');\n this.hiddenTextarea.setAttribute('autocorrect', 'off');\n this.hiddenTextarea.setAttribute('autocomplete', 'off');\n this.hiddenTextarea.setAttribute('spellcheck', 'false');\n this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', '');\n this.hiddenTextarea.setAttribute('wrap', 'off');\n var style = this._calcTextareaPosition();\n this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top +\n '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' +\n ' line-height: 1px; paddingーtop: ' + style.fontSize + ';';\n fabric.document.body.appendChild(this.hiddenTextarea);\n\n fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'cut', this.cut.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this));\n fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this));\n\n if (!this._clickHandlerInitialized && this.canvas) {\n fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this));\n this._clickHandlerInitialized = true;\n }\n },\n\n /**\n * For functionalities on keyDown\n * Map a special key to a function of the instance/prototype\n * If you need different behaviour for ESC or TAB or arrows, you have to change\n * this map setting the name of a function that you build on the fabric.Itext or\n * your prototype.\n * the map change will affect all Instances unless you need for only some text Instances\n * in that case you have to clone this object and assign your Instance.\n * this.keysMap = fabric.util.object.clone(this.keysMap);\n * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0]\n */\n keysMap: {\n 8: 'removeChars',\n 9: 'exitEditing',\n 27: 'exitEditing',\n 13: 'insertNewline',\n 33: 'moveCursorUp',\n 34: 'moveCursorDown',\n 35: 'moveCursorRight',\n 36: 'moveCursorLeft',\n 37: 'moveCursorLeft',\n 38: 'moveCursorUp',\n 39: 'moveCursorRight',\n 40: 'moveCursorDown',\n 46: 'forwardDelete'\n },\n\n /**\n * For functionalities on keyUp + ctrl || cmd\n */\n ctrlKeysMapUp: {\n 67: 'copy',\n 88: 'cut'\n },\n\n /**\n * For functionalities on keyDown + ctrl || cmd\n */\n ctrlKeysMapDown: {\n 65: 'selectAll'\n },\n\n onClick: function() {\n // No need to trigger click event here, focus is enough to have the keyboard appear on Android\n this.hiddenTextarea && this.hiddenTextarea.focus();\n },\n\n /**\n * Handles keyup event\n * @param {Event} e Event object\n */\n onKeyDown: function(e) {\n if (!this.isEditing) {\n return;\n }\n if (e.keyCode in this.keysMap) {\n this[this.keysMap[e.keyCode]](e);\n }\n else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {\n this[this.ctrlKeysMapDown[e.keyCode]](e);\n }\n else {\n return;\n }\n e.stopImmediatePropagation();\n e.preventDefault();\n if (e.keyCode >= 33 && e.keyCode <= 40) {\n // if i press an arrow key just update selection\n this.clearContextTop();\n this.renderCursorOrSelection();\n }\n else {\n this.canvas && this.canvas.renderAll();\n }\n },\n\n /**\n * Handles keyup event\n * We handle KeyUp because ie11 and edge have difficulties copy/pasting\n * if a copy/cut event fired, keyup is dismissed\n * @param {Event} e Event object\n */\n onKeyUp: function(e) {\n if (!this.isEditing || this._copyDone) {\n this._copyDone = false;\n return;\n }\n if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {\n this[this.ctrlKeysMapUp[e.keyCode]](e);\n }\n else {\n return;\n }\n e.stopImmediatePropagation();\n e.preventDefault();\n this.canvas && this.canvas.renderAll();\n },\n\n /**\n * Handles onInput event\n * @param {Event} e Event object\n */\n onInput: function(e) {\n if (!this.isEditing || this.inCompositionMode) {\n return;\n }\n var offset = this.selectionStart || 0,\n offsetEnd = this.selectionEnd || 0,\n textLength = this.text.length,\n newTextLength = this.hiddenTextarea.value.length,\n diff, charsToInsert, start;\n if (newTextLength > textLength) {\n //we added some character\n start = this._selectionDirection === 'left' ? offsetEnd : offset;\n diff = newTextLength - textLength;\n charsToInsert = this.hiddenTextarea.value.slice(start, start + diff);\n }\n else {\n //we selected a portion of text and then input something else.\n //Internet explorer does not trigger this else\n diff = newTextLength - textLength + offsetEnd - offset;\n charsToInsert = this.hiddenTextarea.value.slice(offset, offset + diff);\n }\n this.insertChars(charsToInsert);\n e.stopPropagation();\n },\n\n /**\n * Composition start\n */\n onCompositionStart: function() {\n this.inCompositionMode = true;\n this.prevCompositionLength = 0;\n this.compositionStart = this.selectionStart;\n },\n\n /**\n * Composition end\n */\n onCompositionEnd: function() {\n this.inCompositionMode = false;\n },\n\n /**\n * Composition update\n */\n onCompositionUpdate: function(e) {\n var data = e.data;\n this.selectionStart = this.compositionStart;\n this.selectionEnd = this.selectionEnd === this.selectionStart ?\n this.compositionStart + this.prevCompositionLength : this.selectionEnd;\n this.insertChars(data, false);\n this.prevCompositionLength = data.length;\n },\n\n /**\n * Forward delete\n */\n forwardDelete: function(e) {\n if (this.selectionStart === this.selectionEnd) {\n if (this.selectionStart === this.text.length) {\n return;\n }\n this.moveCursorRight(e);\n }\n this.removeChars(e);\n },\n\n /**\n * Copies selected text\n * @param {Event} e Event object\n */\n copy: function(e) {\n if (this.selectionStart === this.selectionEnd) {\n //do not cut-copy if no selection\n return;\n }\n var selectedText = this.getSelectedText(),\n clipboardData = this._getClipboardData(e);\n\n // Check for backward compatibility with old browsers\n if (clipboardData) {\n clipboardData.setData('text', selectedText);\n }\n\n fabric.copiedText = selectedText;\n fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd);\n e.stopImmediatePropagation();\n e.preventDefault();\n this._copyDone = true;\n },\n\n /**\n * Pastes text\n * @param {Event} e Event object\n */\n paste: function(e) {\n var copiedText = null,\n clipboardData = this._getClipboardData(e),\n useCopiedStyle = true;\n\n // Check for backward compatibility with old browsers\n if (clipboardData) {\n copiedText = clipboardData.getData('text').replace(/\\r/g, '');\n if (!fabric.copiedTextStyle || fabric.copiedText !== copiedText) {\n useCopiedStyle = false;\n }\n }\n else {\n copiedText = fabric.copiedText;\n }\n\n if (copiedText) {\n this.insertChars(copiedText, useCopiedStyle);\n }\n e.stopImmediatePropagation();\n e.preventDefault();\n },\n\n /**\n * Cuts text\n * @param {Event} e Event object\n */\n cut: function(e) {\n if (this.selectionStart === this.selectionEnd) {\n return;\n }\n\n this.copy(e);\n this.removeChars(e);\n },\n\n /**\n * @private\n * @param {Event} e Event object\n * @return {Object} Clipboard data object\n */\n _getClipboardData: function(e) {\n return (e && e.clipboardData) || fabric.window.clipboardData;\n },\n\n /**\n * Finds the width in pixels before the cursor on the same line\n * @private\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @return {Number} widthBeforeCursor width before cursor\n */\n _getWidthBeforeCursor: function(lineIndex, charIndex) {\n var textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex),\n widthOfLine = this._getLineWidth(this.ctx, lineIndex),\n widthBeforeCursor = this._getLineLeftOffset(widthOfLine), _char;\n\n for (var i = 0, len = textBeforeCursor.length; i < len; i++) {\n _char = textBeforeCursor[i];\n widthBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);\n }\n return widthBeforeCursor;\n },\n\n /**\n * Gets start offset of a selection\n * @param {Event} e Event object\n * @param {Boolean} isRight\n * @return {Number}\n */\n getDownCursorOffset: function(e, isRight) {\n var selectionProp = this._getSelectionForOffset(e, isRight),\n cursorLocation = this.get2DCursorLocation(selectionProp),\n lineIndex = cursorLocation.lineIndex;\n // if on last line, down cursor goes to end of line\n if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {\n // move to the end of a text\n return this.text.length - selectionProp;\n }\n var charIndex = cursorLocation.charIndex,\n widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),\n indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),\n textAfterCursor = this._textLines[lineIndex].slice(charIndex);\n\n return textAfterCursor.length + indexOnOtherLine + 2;\n },\n\n /**\n * private\n * Helps finding if the offset should be counted from Start or End\n * @param {Event} e Event object\n * @param {Boolean} isRight\n * @return {Number}\n */\n _getSelectionForOffset: function(e, isRight) {\n if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {\n return this.selectionEnd;\n }\n else {\n return this.selectionStart;\n }\n },\n\n /**\n * @param {Event} e Event object\n * @param {Boolean} isRight\n * @return {Number}\n */\n getUpCursorOffset: function(e, isRight) {\n var selectionProp = this._getSelectionForOffset(e, isRight),\n cursorLocation = this.get2DCursorLocation(selectionProp),\n lineIndex = cursorLocation.lineIndex;\n if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {\n // if on first line, up cursor goes to start of line\n return -selectionProp;\n }\n var charIndex = cursorLocation.charIndex,\n widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),\n indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor),\n textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex);\n // return a negative offset\n return -this._textLines[lineIndex - 1].length + indexOnOtherLine - textBeforeCursor.length;\n },\n\n /**\n * find for a given width it founds the matching character.\n * @private\n */\n _getIndexOnLine: function(lineIndex, width) {\n\n var widthOfLine = this._getLineWidth(this.ctx, lineIndex),\n textOnLine = this._textLines[lineIndex],\n lineLeftOffset = this._getLineLeftOffset(widthOfLine),\n widthOfCharsOnLine = lineLeftOffset,\n indexOnLine = 0,\n foundMatch;\n\n for (var j = 0, jlen = textOnLine.length; j < jlen; j++) {\n\n var _char = textOnLine[j],\n widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);\n\n widthOfCharsOnLine += widthOfChar;\n\n if (widthOfCharsOnLine > width) {\n\n foundMatch = true;\n\n var leftEdge = widthOfCharsOnLine - widthOfChar,\n rightEdge = widthOfCharsOnLine,\n offsetFromLeftEdge = Math.abs(leftEdge - width),\n offsetFromRightEdge = Math.abs(rightEdge - width);\n\n indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);\n\n break;\n }\n }\n\n // reached end\n if (!foundMatch) {\n indexOnLine = textOnLine.length - 1;\n }\n\n return indexOnLine;\n },\n\n\n /**\n * Moves cursor down\n * @param {Event} e Event object\n */\n moveCursorDown: function(e) {\n if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) {\n return;\n }\n this._moveCursorUpOrDown('Down', e);\n },\n\n /**\n * Moves cursor up\n * @param {Event} e Event object\n */\n moveCursorUp: function(e) {\n if (this.selectionStart === 0 && this.selectionEnd === 0) {\n return;\n }\n this._moveCursorUpOrDown('Up', e);\n },\n\n /**\n * Moves cursor up or down, fires the events\n * @param {String} direction 'Up' or 'Down'\n * @param {Event} e Event object\n */\n _moveCursorUpOrDown: function(direction, e) {\n // getUpCursorOffset\n // getDownCursorOffset\n var action = 'get' + direction + 'CursorOffset',\n offset = this[action](e, this._selectionDirection === 'right');\n if (e.shiftKey) {\n this.moveCursorWithShift(offset);\n }\n else {\n this.moveCursorWithoutShift(offset);\n }\n if (offset !== 0) {\n this.setSelectionInBoundaries();\n this.abortCursorAnimation();\n this._currentCursorOpacity = 1;\n this.initDelayedCursor();\n this._fireSelectionChanged();\n this._updateTextarea();\n }\n },\n\n /**\n * Moves cursor with shift\n * @param {Number} offset\n */\n moveCursorWithShift: function(offset) {\n var newSelection = this._selectionDirection === 'left'\n ? this.selectionStart + offset\n : this.selectionEnd + offset;\n this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection);\n return offset !== 0;\n },\n\n /**\n * Moves cursor up without shift\n * @param {Number} offset\n */\n moveCursorWithoutShift: function(offset) {\n if (offset < 0) {\n this.selectionStart += offset;\n this.selectionEnd = this.selectionStart;\n }\n else {\n this.selectionEnd += offset;\n this.selectionStart = this.selectionEnd;\n }\n return offset !== 0;\n },\n\n /**\n * Moves cursor left\n * @param {Event} e Event object\n */\n moveCursorLeft: function(e) {\n if (this.selectionStart === 0 && this.selectionEnd === 0) {\n return;\n }\n this._moveCursorLeftOrRight('Left', e);\n },\n\n /**\n * @private\n * @return {Boolean} true if a change happened\n */\n _move: function(e, prop, direction) {\n var newValue;\n if (e.altKey) {\n newValue = this['findWordBoundary' + direction](this[prop]);\n }\n else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) {\n newValue = this['findLineBoundary' + direction](this[prop]);\n }\n else {\n this[prop] += direction === 'Left' ? -1 : 1;\n return true;\n }\n if (typeof newValue !== undefined && this[prop] !== newValue) {\n this[prop] = newValue;\n return true;\n }\n },\n\n /**\n * @private\n */\n _moveLeft: function(e, prop) {\n return this._move(e, prop, 'Left');\n },\n\n /**\n * @private\n */\n _moveRight: function(e, prop) {\n return this._move(e, prop, 'Right');\n },\n\n /**\n * Moves cursor left without keeping selection\n * @param {Event} e\n */\n moveCursorLeftWithoutShift: function(e) {\n var change = true;\n this._selectionDirection = 'left';\n\n // only move cursor when there is no selection,\n // otherwise we discard it, and leave cursor on same place\n if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) {\n change = this._moveLeft(e, 'selectionStart');\n\n }\n this.selectionEnd = this.selectionStart;\n return change;\n },\n\n /**\n * Moves cursor left while keeping selection\n * @param {Event} e\n */\n moveCursorLeftWithShift: function(e) {\n if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {\n return this._moveLeft(e, 'selectionEnd');\n }\n else if (this.selectionStart !== 0){\n this._selectionDirection = 'left';\n return this._moveLeft(e, 'selectionStart');\n }\n },\n\n /**\n * Moves cursor right\n * @param {Event} e Event object\n */\n moveCursorRight: function(e) {\n if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) {\n return;\n }\n this._moveCursorLeftOrRight('Right', e);\n },\n\n /**\n * Moves cursor right or Left, fires event\n * @param {String} direction 'Left', 'Right'\n * @param {Event} e Event object\n */\n _moveCursorLeftOrRight: function(direction, e) {\n var actionName = 'moveCursor' + direction + 'With';\n this._currentCursorOpacity = 1;\n\n if (e.shiftKey) {\n actionName += 'Shift';\n }\n else {\n actionName += 'outShift';\n }\n if (this[actionName](e)) {\n this.abortCursorAnimation();\n this.initDelayedCursor();\n this._fireSelectionChanged();\n this._updateTextarea();\n }\n },\n\n /**\n * Moves cursor right while keeping selection\n * @param {Event} e\n */\n moveCursorRightWithShift: function(e) {\n if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {\n return this._moveRight(e, 'selectionStart');\n }\n else if (this.selectionEnd !== this.text.length) {\n this._selectionDirection = 'right';\n return this._moveRight(e, 'selectionEnd');\n }\n },\n\n /**\n * Moves cursor right without keeping selection\n * @param {Event} e Event object\n */\n moveCursorRightWithoutShift: function(e) {\n var changed = true;\n this._selectionDirection = 'right';\n\n if (this.selectionStart === this.selectionEnd) {\n changed = this._moveRight(e, 'selectionStart');\n this.selectionEnd = this.selectionStart;\n }\n else {\n this.selectionStart = this.selectionEnd;\n }\n return changed;\n },\n\n /**\n * Removes characters selected by selection\n * @param {Event} e Event object\n */\n removeChars: function(e) {\n if (this.selectionStart === this.selectionEnd) {\n this._removeCharsNearCursor(e);\n }\n else {\n this._removeCharsFromTo(this.selectionStart, this.selectionEnd);\n }\n\n this.set('dirty', true);\n this.setSelectionEnd(this.selectionStart);\n\n this._removeExtraneousStyles();\n\n this.canvas && this.canvas.renderAll();\n\n this.setCoords();\n this.fire('changed');\n this.canvas && this.canvas.fire('text:changed', { target: this });\n },\n\n /**\n * @private\n * @param {Event} e Event object\n */\n _removeCharsNearCursor: function(e) {\n if (this.selectionStart === 0) {\n return;\n }\n if (e.metaKey) {\n // remove all till the start of current line\n var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart);\n\n this._removeCharsFromTo(leftLineBoundary, this.selectionStart);\n this.setSelectionStart(leftLineBoundary);\n }\n else if (e.altKey) {\n // remove all till the start of current word\n var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart);\n\n this._removeCharsFromTo(leftWordBoundary, this.selectionStart);\n this.setSelectionStart(leftWordBoundary);\n }\n else {\n this._removeSingleCharAndStyle(this.selectionStart);\n this.setSelectionStart(this.selectionStart - 1);\n }\n }\n});\n\n\n/* _TO_SVG_START_ */\n(function() {\n var toFixed = fabric.util.toFixed,\n NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;\n\n fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {\n\n /**\n * @private\n */\n _setSVGTextLineText: function(lineIndex, textSpans, height, textLeftOffset, textTopOffset, textBgRects) {\n if (!this._getLineStyle(lineIndex)) {\n fabric.Text.prototype._setSVGTextLineText.call(this,\n lineIndex, textSpans, height, textLeftOffset, textTopOffset);\n }\n else {\n this._setSVGTextLineChars(\n lineIndex, textSpans, height, textLeftOffset, textBgRects);\n }\n },\n\n /**\n * @private\n */\n _setSVGTextLineChars: function(lineIndex, textSpans, height, textLeftOffset, textBgRects) {\n\n var chars = this._textLines[lineIndex],\n charOffset = 0,\n lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)) - this.width / 2,\n lineOffset = this._getSVGLineTopOffset(lineIndex),\n heightOfLine = this._getHeightOfLine(this.ctx, lineIndex);\n\n for (var i = 0, len = chars.length; i < len; i++) {\n var styleDecl = this._getStyleDeclaration(lineIndex, i) || { };\n\n textSpans.push(\n this._createTextCharSpan(\n chars[i], styleDecl, lineLeftOffset, lineOffset.lineTop + lineOffset.offset, charOffset));\n\n var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i);\n\n if (styleDecl.textBackgroundColor) {\n textBgRects.push(\n this._createTextCharBg(\n styleDecl, lineLeftOffset, lineOffset.lineTop, heightOfLine, charWidth, charOffset));\n }\n\n charOffset += charWidth;\n }\n },\n\n /**\n * @private\n */\n _getSVGLineTopOffset: function(lineIndex) {\n var lineTopOffset = 0, lastHeight = 0;\n for (var j = 0; j < lineIndex; j++) {\n lineTopOffset += this._getHeightOfLine(this.ctx, j);\n }\n lastHeight = this._getHeightOfLine(this.ctx, j);\n return {\n lineTop: lineTopOffset,\n offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)\n };\n },\n\n /**\n * @private\n */\n _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) {\n return [\n '\\t\\t\\n'\n ].join('');\n },\n\n /**\n * @private\n */\n _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, charOffset) {\n\n var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({\n visible: true,\n fill: this.fill,\n stroke: this.stroke,\n type: 'text',\n getSvgFilter: fabric.Object.prototype.getSvgFilter\n }, styleDecl));\n\n return [\n '\\t\\t\\t',\n fabric.util.string.escapeXml(_char),\n '\\n'\n ].join('');\n },\n });\n})();\n/* _TO_SVG_END_ */\n\n\n(function(global) {\n\n 'use strict';\n\n var fabric = global.fabric || (global.fabric = {});\n\n /**\n * Textbox class, based on IText, allows the user to resize the text rectangle\n * and wraps lines automatically. Textboxes have their Y scaling locked, the\n * user can only change width. Height is adjusted automatically based on the\n * wrapping of lines.\n * @class fabric.Textbox\n * @extends fabric.IText\n * @mixes fabric.Observable\n * @return {fabric.Textbox} thisArg\n * @see {@link fabric.Textbox#initialize} for constructor definition\n */\n fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {\n\n /**\n * Type of an object\n * @type String\n * @default\n */\n type: 'textbox',\n\n /**\n * Minimum width of textbox, in pixels.\n * @type Number\n * @default\n */\n minWidth: 20,\n\n /**\n * Minimum calculated width of a textbox, in pixels.\n * fixed to 2 so that an empty textbox cannot go to 0\n * and is still selectable without text.\n * @type Number\n * @default\n */\n dynamicMinWidth: 2,\n\n /**\n * Cached array of text wrapping.\n * @type Array\n */\n __cachedLines: null,\n\n /**\n * Override standard Object class values\n */\n lockScalingY: true,\n\n /**\n * Override standard Object class values\n */\n lockScalingFlip: true,\n\n /**\n * Override standard Object class values\n * Textbox needs this on false\n */\n noScaleCache: false,\n\n /**\n * Properties which when set cause object to change dimensions\n * @type Object\n * @private\n */\n _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'),\n\n /**\n * Constructor. Some scaling related property values are forced. Visibility\n * of controls is also fixed; only the rotation and width controls are\n * made available.\n * @param {String} text Text string\n * @param {Object} [options] Options object\n * @return {fabric.Textbox} thisArg\n */\n initialize: function(text, options) {\n\n this.callSuper('initialize', text, options);\n this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility());\n this.ctx = this.objectCaching ? this._cacheContext : fabric.util.createCanvasElement().getContext('2d');\n },\n\n /**\n * Unlike superclass's version of this function, Textbox does not update\n * its width.\n * @param {CanvasRenderingContext2D} ctx Context to use for measurements\n * @private\n * @override\n */\n _initDimensions: function(ctx) {\n if (this.__skipDimension) {\n return;\n }\n\n if (!ctx) {\n ctx = fabric.util.createCanvasElement().getContext('2d');\n this._setTextStyles(ctx);\n this.clearContextTop();\n }\n\n // clear dynamicMinWidth as it will be different after we re-wrap line\n this.dynamicMinWidth = 0;\n\n // wrap lines\n this._textLines = this._splitTextIntoLines(ctx);\n // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap\n if (this.dynamicMinWidth > this.width) {\n this._set('width', this.dynamicMinWidth);\n }\n\n // clear cache and re-calculate height\n this._clearCache();\n this.height = this._getTextHeight(ctx);\n this.setCoords();\n },\n\n /**\n * Generate an object that translates the style object so that it is\n * broken up by visual lines (new lines and automatic wrapping).\n * The original text styles object is broken up by actual lines (new lines only),\n * which is only sufficient for Text / IText\n * @private\n */\n _generateStyleMap: function() {\n var realLineCount = 0,\n realLineCharCount = 0,\n charCount = 0,\n map = {};\n\n for (var i = 0; i < this._textLines.length; i++) {\n if (this.text[charCount] === '\\n' && i > 0) {\n realLineCharCount = 0;\n charCount++;\n realLineCount++;\n }\n else if (this.text[charCount] === ' ' && i > 0) {\n // this case deals with space's that are removed from end of lines when wrapping\n realLineCharCount++;\n charCount++;\n }\n\n map[i] = { line: realLineCount, offset: realLineCharCount };\n\n charCount += this._textLines[i].length;\n realLineCharCount += this._textLines[i].length;\n }\n\n return map;\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @param {Boolean} [returnCloneOrEmpty=false]\n * @private\n */\n _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) {\n if (this._styleMap) {\n var map = this._styleMap[lineIndex];\n if (!map) {\n return returnCloneOrEmpty ? { } : null;\n }\n lineIndex = map.line;\n charIndex = map.offset + charIndex;\n }\n return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty);\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @param {Object} style\n * @private\n */\n _setStyleDeclaration: function(lineIndex, charIndex, style) {\n var map = this._styleMap[lineIndex];\n lineIndex = map.line;\n charIndex = map.offset + charIndex;\n\n this.styles[lineIndex][charIndex] = style;\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Number} charIndex\n * @private\n */\n _deleteStyleDeclaration: function(lineIndex, charIndex) {\n var map = this._styleMap[lineIndex];\n lineIndex = map.line;\n charIndex = map.offset + charIndex;\n\n delete this.styles[lineIndex][charIndex];\n },\n\n /**\n * @param {Number} lineIndex\n * @private\n */\n _getLineStyle: function(lineIndex) {\n var map = this._styleMap[lineIndex];\n return this.styles[map.line];\n },\n\n /**\n * @param {Number} lineIndex\n * @param {Object} style\n * @private\n */\n _setLineStyle: function(lineIndex, style) {\n var map = this._styleMap[lineIndex];\n this.styles[map.line] = style;\n },\n\n /**\n * @param {Number} lineIndex\n * @private\n */\n _deleteLineStyle: function(lineIndex) {\n var map = this._styleMap[lineIndex];\n delete this.styles[map.line];\n },\n\n /**\n * Wraps text using the 'width' property of Textbox. First this function\n * splits text on newlines, so we preserve newlines entered by the user.\n * Then it wraps each line using the width of the Textbox by calling\n * _wrapLine().\n * @param {CanvasRenderingContext2D} ctx Context to use for measurements\n * @param {String} text The string of text that is split into lines\n * @returns {Array} Array of lines\n */\n _wrapText: function(ctx, text) {\n var lines = text.split(this._reNewline), wrapped = [], i;\n\n for (i = 0; i < lines.length; i++) {\n wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i));\n }\n\n return wrapped;\n },\n\n /**\n * Helper function to measure a string of text, given its lineIndex and charIndex offset\n *\n * @param {CanvasRenderingContext2D} ctx\n * @param {String} text\n * @param {number} lineIndex\n * @param {number} charOffset\n * @returns {number}\n * @private\n */\n _measureText: function(ctx, text, lineIndex, charOffset) {\n var width = 0;\n charOffset = charOffset || 0;\n for (var i = 0, len = text.length; i < len; i++) {\n width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset);\n }\n return width;\n },\n\n /**\n * Wraps a line of text using the width of the Textbox and a context.\n * @param {CanvasRenderingContext2D} ctx Context to use for measurements\n * @param {String} text The string of text to split into lines\n * @param {Number} lineIndex\n * @returns {Array} Array of line(s) into which the given text is wrapped\n * to.\n */\n _wrapLine: function(ctx, text, lineIndex) {\n var lineWidth = 0,\n lines = [],\n line = '',\n words = text.split(' '),\n word = '',\n offset = 0,\n infix = ' ',\n wordWidth = 0,\n infixWidth = 0,\n largestWordWidth = 0,\n lineJustStarted = true,\n additionalSpace = this._getWidthOfCharSpacing();\n\n for (var i = 0; i < words.length; i++) {\n word = words[i];\n wordWidth = this._measureText(ctx, word, lineIndex, offset);\n\n offset += word.length;\n\n lineWidth += infixWidth + wordWidth - additionalSpace;\n\n if (lineWidth >= this.width && !lineJustStarted) {\n lines.push(line);\n line = '';\n lineWidth = wordWidth;\n lineJustStarted = true;\n }\n else {\n lineWidth += additionalSpace;\n }\n\n if (!lineJustStarted) {\n line += infix;\n }\n line += word;\n\n infixWidth = this._measureText(ctx, infix, lineIndex, offset);\n offset++;\n lineJustStarted = false;\n // keep track of largest word\n if (wordWidth > largestWordWidth) {\n largestWordWidth = wordWidth;\n }\n }\n\n i && lines.push(line);\n\n if (largestWordWidth > this.dynamicMinWidth) {\n this.dynamicMinWidth = largestWordWidth - additionalSpace;\n }\n\n return lines;\n },\n /**\n * Gets lines of text to render in the Textbox. This function calculates\n * text wrapping on the fly everytime it is called.\n * @returns {Array} Array of lines in the Textbox.\n * @override\n */\n _splitTextIntoLines: function(ctx) {\n ctx = ctx || this.ctx;\n var originalAlign = this.textAlign;\n this._styleMap = null;\n ctx.save();\n this._setTextStyles(ctx);\n this.textAlign = 'left';\n var lines = this._wrapText(ctx, this.text);\n this.textAlign = originalAlign;\n ctx.restore();\n this._textLines = lines;\n this._styleMap = this._generateStyleMap();\n return lines;\n },\n\n /**\n * When part of a group, we don't want the Textbox's scale to increase if\n * the group's increases. That's why we reduce the scale of the Textbox by\n * the amount that the group's increases. This is to maintain the effective\n * scale of the Textbox at 1, so that font-size values make sense. Otherwise\n * the same font-size value would result in different actual size depending\n * on the value of the scale.\n * @param {String} key\n * @param {*} value\n */\n setOnGroup: function(key, value) {\n if (key === 'scaleX') {\n this.set('scaleX', Math.abs(1 / value));\n this.set('width', (this.get('width') * value) /\n (typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX));\n this.__oldScaleX = value;\n }\n },\n\n /**\n * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start).\n * Overrides the superclass function to take into account text wrapping.\n *\n * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.\n */\n get2DCursorLocation: function(selectionStart) {\n if (typeof selectionStart === 'undefined') {\n selectionStart = this.selectionStart;\n }\n\n var numLines = this._textLines.length,\n removed = 0;\n\n for (var i = 0; i < numLines; i++) {\n var line = this._textLines[i],\n lineLen = line.length;\n\n if (selectionStart <= removed + lineLen) {\n return {\n lineIndex: i,\n charIndex: selectionStart - removed\n };\n }\n\n removed += lineLen;\n\n if (this.text[removed] === '\\n' || this.text[removed] === ' ') {\n removed++;\n }\n }\n\n return {\n lineIndex: numLines - 1,\n charIndex: this._textLines[numLines - 1].length\n };\n },\n\n /**\n * Overrides superclass function and uses text wrapping data to get cursor\n * boundary offsets instead of the array of chars.\n * @param {Array} chars Unused\n * @param {String} typeOfBoundaries Can be 'cursor' or 'selection'\n * @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set.\n */\n _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) {\n var topOffset = 0,\n leftOffset = 0,\n cursorLocation = this.get2DCursorLocation(),\n lineChars = this._textLines[cursorLocation.lineIndex].split(''),\n lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex));\n\n for (var i = 0; i < cursorLocation.charIndex; i++) {\n leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i);\n }\n\n for (i = 0; i < cursorLocation.lineIndex; i++) {\n topOffset += this._getHeightOfLine(this.ctx, i);\n }\n\n if (typeOfBoundaries === 'cursor') {\n topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex)\n / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)\n * (1 - this._fontSizeFraction);\n }\n\n return {\n top: topOffset,\n left: leftOffset,\n lineLeft: lineLeftOffset\n };\n },\n\n getMinWidth: function() {\n return Math.max(this.minWidth, this.dynamicMinWidth);\n },\n\n /**\n * Returns object representation of an instance\n * @method toObject\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @return {Object} object representation of an instance\n */\n toObject: function(propertiesToInclude) {\n return this.callSuper('toObject', ['minWidth'].concat(propertiesToInclude));\n }\n });\n\n /**\n * Returns fabric.Textbox instance from an object representation\n * @static\n * @memberOf fabric.Textbox\n * @param {Object} object Object to create an instance from\n * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created\n * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first\n * @return {fabric.Textbox} instance of fabric.Textbox\n */\n fabric.Textbox.fromObject = function(object, callback, forceAsync) {\n return fabric.Object._fromObject('Textbox', object, callback, forceAsync, 'text');\n };\n\n /**\n * Returns the default controls visibility required for Textboxes.\n * @returns {Object}\n */\n fabric.Textbox.getTextboxControlVisibility = function() {\n return {\n tl: false,\n tr: false,\n br: false,\n bl: false,\n ml: true,\n mt: false,\n mr: true,\n mb: false,\n mtr: true\n };\n };\n\n})(typeof exports !== 'undefined' ? exports : this);\n\n\n(function() {\n\n /**\n * Override _setObjectScale and add Textbox specific resizing behavior. Resizing\n * a Textbox doesn't scale text, it only changes width and makes text wrap automatically.\n */\n var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale;\n\n fabric.Canvas.prototype._setObjectScale = function(localMouse, transform,\n lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {\n\n var t = transform.target;\n if (t instanceof fabric.Textbox) {\n var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth));\n if (w >= t.getMinWidth()) {\n t.set('width', w);\n return true;\n }\n }\n else {\n return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform,\n lockScalingX, lockScalingY, by, lockScalingFlip, _dim);\n }\n };\n\n /**\n * Sets controls of this group to the Textbox's special configuration if\n * one is present in the group. Deletes _controlsVisibility otherwise, so that\n * it gets initialized to default value at runtime.\n */\n fabric.Group.prototype._refreshControlsVisibility = function() {\n if (typeof fabric.Textbox === 'undefined') {\n return;\n }\n for (var i = this._objects.length; i--;) {\n if (this._objects[i] instanceof fabric.Textbox) {\n this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility());\n return;\n }\n }\n };\n\n fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.IText.prototype */ {\n /**\n * @private\n */\n _removeExtraneousStyles: function() {\n for (var prop in this._styleMap) {\n if (!this._textLines[prop]) {\n delete this.styles[this._styleMap[prop].line];\n }\n }\n },\n\n /**\n * Inserts style object for a given line/char index\n * @param {Number} lineIndex Index of a line\n * @param {Number} charIndex Index of a char\n * @param {Object} [style] Style object to insert, if given\n */\n insertCharStyleObject: function(lineIndex, charIndex, style) {\n // adjust lineIndex and charIndex\n var map = this._styleMap[lineIndex];\n lineIndex = map.line;\n charIndex = map.offset + charIndex;\n\n fabric.IText.prototype.insertCharStyleObject.apply(this, [lineIndex, charIndex, style]);\n },\n\n /**\n * Inserts new style object\n * @param {Number} lineIndex Index of a line\n * @param {Number} charIndex Index of a char\n * @param {Boolean} isEndOfLine True if it's end of line\n */\n insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) {\n // adjust lineIndex and charIndex\n var map = this._styleMap[lineIndex];\n lineIndex = map.line;\n charIndex = map.offset + charIndex;\n\n fabric.IText.prototype.insertNewlineStyleObject.apply(this, [lineIndex, charIndex, isEndOfLine]);\n },\n\n /**\n * Shifts line styles up or down. This function is slightly different than the one in\n * itext_behaviour as it takes into account the styleMap.\n *\n * @param {Number} lineIndex Index of a line\n * @param {Number} offset Can be -1 or +1\n */\n shiftLineStyles: function(lineIndex, offset) {\n // shift all line styles by 1 upward\n var map = this._styleMap[lineIndex];\n // adjust line index\n lineIndex = map.line;\n fabric.IText.prototype.shiftLineStyles.call(this, lineIndex, offset);\n },\n\n /**\n * Figure out programatically the text on previous actual line (actual = separated by \\n);\n *\n * @param {Number} lIndex\n * @returns {String}\n * @private\n */\n _getTextOnPreviousLine: function(lIndex) {\n var textOnPreviousLine = this._textLines[lIndex - 1];\n\n while (this._styleMap[lIndex - 2] && this._styleMap[lIndex - 2].line === this._styleMap[lIndex - 1].line) {\n textOnPreviousLine = this._textLines[lIndex - 2] + textOnPreviousLine;\n\n lIndex--;\n }\n\n return textOnPreviousLine;\n },\n\n /**\n * Removes style object\n * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line\n * @param {Number} [index] Optional index. When not given, current selectionStart is used.\n */\n removeStyleObject: function(isBeginningOfLine, index) {\n\n var cursorLocation = this.get2DCursorLocation(index),\n map = this._styleMap[cursorLocation.lineIndex],\n lineIndex = map.line,\n charIndex = map.offset + cursorLocation.charIndex;\n this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex);\n }\n });\n})();\n\n\n(function() {\n var override = fabric.IText.prototype._getNewSelectionStartFromOffset;\n /**\n * Overrides the IText implementation and adjusts character index as there is not always a linebreak\n *\n * @param {Number} mouseOffset\n * @param {Number} prevWidth\n * @param {Number} width\n * @param {Number} index\n * @param {Number} jlen\n * @returns {Number}\n */\n fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, prevWidth, width, index, jlen) {\n index = override.call(this, mouseOffset, prevWidth, width, index, jlen);\n\n // the index passed into the function is padded by the amount of lines from _textLines (to account for \\n)\n // we need to remove this padding, and pad it by actual lines, and / or spaces that are meant to be there\n var tmp = 0,\n removed = 0;\n\n // account for removed characters\n for (var i = 0; i < this._textLines.length; i++) {\n tmp += this._textLines[i].length;\n\n if (tmp + removed >= index) {\n break;\n }\n\n if (this.text[tmp + removed] === '\\n' || this.text[tmp + removed] === ' ') {\n removed++;\n }\n }\n\n return index - i + removed;\n };\n})();\n\n\n(function() {\n\n if (typeof document !== 'undefined' && typeof window !== 'undefined') {\n return;\n }\n\n var DOMParser = require('xmldom').DOMParser,\n URL = require('url'),\n HTTP = require('http'),\n HTTPS = require('https'),\n\n Canvas = require('canvas'),\n Image = require('canvas').Image;\n\n /** @private */\n function request(url, encoding, callback) {\n var oURL = URL.parse(url);\n\n // detect if http or https is used\n if ( !oURL.port ) {\n oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80;\n }\n\n // assign request handler based on protocol\n var reqHandler = (oURL.protocol.indexOf('https:') === 0 ) ? HTTPS : HTTP,\n req = reqHandler.request({\n hostname: oURL.hostname,\n port: oURL.port,\n path: oURL.path,\n method: 'GET'\n }, function(response) {\n var body = '';\n if (encoding) {\n response.setEncoding(encoding);\n }\n response.on('end', function () {\n callback(body);\n });\n response.on('data', function (chunk) {\n if (response.statusCode === 200) {\n body += chunk;\n }\n });\n });\n\n req.on('error', function(err) {\n if (err.errno === process.ECONNREFUSED) {\n fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port);\n }\n else {\n fabric.log(err.message);\n }\n callback(null);\n });\n\n req.end();\n }\n\n /** @private */\n function requestFs(path, callback) {\n var fs = require('fs');\n fs.readFile(path, function (err, data) {\n if (err) {\n fabric.log(err);\n throw err;\n }\n else {\n callback(data);\n }\n });\n }\n\n fabric.util.loadImage = function(url, callback, context) {\n function createImageAndCallBack(data) {\n if (data) {\n img.src = new Buffer(data, 'binary');\n // preserving original url, which seems to be lost in node-canvas\n img._src = url;\n callback && callback.call(context, img);\n }\n else {\n img = null;\n callback && callback.call(context, null, true);\n }\n }\n var img = new Image();\n if (url && (url instanceof Buffer || url.indexOf('data') === 0)) {\n img.src = img._src = url;\n callback && callback.call(context, img);\n }\n else if (url && url.indexOf('http') !== 0) {\n requestFs(url, createImageAndCallBack);\n }\n else if (url) {\n request(url, 'binary', createImageAndCallBack);\n }\n else {\n callback && callback.call(context, url);\n }\n };\n\n fabric.loadSVGFromURL = function(url, callback, reviver) {\n url = url.replace(/^\\n\\s*/, '').replace(/\\?.*$/, '').trim();\n if (url.indexOf('http') !== 0) {\n requestFs(url, function(body) {\n fabric.loadSVGFromString(body.toString(), callback, reviver);\n });\n }\n else {\n request(url, '', function(body) {\n fabric.loadSVGFromString(body, callback, reviver);\n });\n }\n };\n\n fabric.loadSVGFromString = function(string, callback, reviver) {\n var doc = new DOMParser().parseFromString(string);\n fabric.parseSVGDocument(doc.documentElement, function(results, options) {\n callback && callback(results, options);\n }, reviver);\n };\n\n fabric.util.getScript = function(url, callback) {\n request(url, '', function(body) {\n // eslint-disable-next-line no-eval\n eval(body);\n callback && callback();\n });\n };\n\n // fabric.util.createCanvasElement = function(_, width, height) {\n // return new Canvas(width, height);\n // }\n\n /**\n * Only available when running fabric on node.js\n * @param {Number} width Canvas width\n * @param {Number} height Canvas height\n * @param {Object} [options] Options to pass to FabricCanvas.\n * @param {Object} [nodeCanvasOptions] Options to pass to NodeCanvas.\n * @return {Object} wrapped canvas instance\n */\n fabric.createCanvasForNode = function(width, height, options, nodeCanvasOptions) {\n nodeCanvasOptions = nodeCanvasOptions || options;\n\n var canvasEl = fabric.document.createElement('canvas'),\n nodeCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions),\n nodeCacheCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions);\n\n // jsdom doesn't create style on canvas element, so here be temp. workaround\n canvasEl.style = { };\n\n canvasEl.width = nodeCanvas.width;\n canvasEl.height = nodeCanvas.height;\n options = options || { };\n options.nodeCanvas = nodeCanvas;\n options.nodeCacheCanvas = nodeCacheCanvas;\n var FabricCanvas = fabric.Canvas || fabric.StaticCanvas,\n fabricCanvas = new FabricCanvas(canvasEl, options);\n fabricCanvas.nodeCanvas = nodeCanvas;\n fabricCanvas.nodeCacheCanvas = nodeCacheCanvas;\n fabricCanvas.contextContainer = nodeCanvas.getContext('2d');\n fabricCanvas.contextCache = nodeCacheCanvas.getContext('2d');\n fabricCanvas.Font = Canvas.Font;\n return fabricCanvas;\n };\n\n var originaInitStatic = fabric.StaticCanvas.prototype._initStatic;\n fabric.StaticCanvas.prototype._initStatic = function(el, options) {\n el = el || fabric.document.createElement('canvas');\n this.nodeCanvas = new Canvas(el.width, el.height);\n this.nodeCacheCanvas = new Canvas(el.width, el.height);\n originaInitStatic.call(this, el, options);\n this.contextContainer = this.nodeCanvas.getContext('2d');\n this.contextCache = this.nodeCacheCanvas.getContext('2d');\n this.Font = Canvas.Font;\n };\n\n /** @ignore */\n fabric.StaticCanvas.prototype.createPNGStream = function() {\n return this.nodeCanvas.createPNGStream();\n };\n\n fabric.StaticCanvas.prototype.createJPEGStream = function(opts) {\n return this.nodeCanvas.createJPEGStream(opts);\n };\n\n fabric.StaticCanvas.prototype._initRetinaScaling = function() {\n if (!this._isRetinaScaling()) {\n return;\n }\n\n this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio);\n this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio);\n this.nodeCanvas.width = this.width * fabric.devicePixelRatio;\n this.nodeCanvas.height = this.height * fabric.devicePixelRatio;\n this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio);\n return this;\n };\n if (fabric.Canvas) {\n fabric.Canvas.prototype._initRetinaScaling = fabric.StaticCanvas.prototype._initRetinaScaling;\n }\n\n var origSetBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension;\n fabric.StaticCanvas.prototype._setBackstoreDimension = function(prop, value) {\n origSetBackstoreDimension.call(this, prop, value);\n this.nodeCanvas[prop] = value;\n return this;\n };\n if (fabric.Canvas) {\n fabric.Canvas.prototype._setBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension;\n }\n\n})();\n\n"]}