';
},
/**
* Return the dimension and the zoom level needed to create a cache canvas
* big enough to host the object to be cached.
* @private
* @param {Object} dim.x width of object to be cached
* @param {Object} dim.y height of object to be cached
* @return {Object}.width width of canvas
* @return {Object}.height height of canvas
* @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
* @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache
*/
_getCacheCanvasDimensions: function() {
var dims = this.callSuper('_getCacheCanvasDimensions');
var fontSize = this.fontSize;
dims.width += fontSize * dims.zoomX;
dims.height += fontSize * dims.zoomY;
return dims;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
this._setTextStyles(ctx);
this._renderTextLinesBackground(ctx);
this._renderTextDecoration(ctx, 'underline');
this._renderText(ctx);
this._renderTextDecoration(ctx, 'overline');
this._renderTextDecoration(ctx, 'linethrough');
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderText: function(ctx) {
if (this.paintFirst === 'stroke') {
this._renderTextStroke(ctx);
this._renderTextFill(ctx);
}
else {
this._renderTextFill(ctx);
this._renderTextStroke(ctx);
}
},
/**
* Set the font parameter of the context with the object properties or with charStyle
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Object} [charStyle] object with font style properties
* @param {String} [charStyle.fontFamily] Font Family
* @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix )
* @param {String} [charStyle.fontWeight] Font weight
* @param {String} [charStyle.fontStyle] Font style (italic|normal)
*/
_setTextStyles: function(ctx, charStyle, forMeasuring) {
ctx.textBaseline = 'alphabetic';
ctx.font = this._getFontDeclaration(charStyle, forMeasuring);
},
/**
* calculate and return the text Width measuring each line.
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @return {Number} Maximum width of fabric.Text object
*/
calcTextWidth: function() {
var maxWidth = this.getLineWidth(0);
for (var i = 1, len = this._textLines.length; i < len; i++) {
var currentLineWidth = this.getLineWidth(i);
if (currentLineWidth > maxWidth) {
maxWidth = currentLineWidth;
}
}
return maxWidth;
},
/**
* @private
* @param {String} method Method name ("fillText" or "strokeText")
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line Text to render
* @param {Number} left Left position of text
* @param {Number} top Top position of text
* @param {Number} lineIndex Index of a line in a text
*/
_renderTextLine: function(method, ctx, line, left, top, lineIndex) {
this._renderChars(method, ctx, line, left, top, lineIndex);
},
/**
* Renders the text background for lines, taking care of style
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextLinesBackground: function(ctx) {
if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) {
return;
}
var lineTopOffset = 0, heightOfLine,
lineLeftOffset, originalFill = ctx.fillStyle,
line, lastColor,
leftOffset = this._getLeftOffset(),
topOffset = this._getTopOffset(),
boxStart = 0, boxWidth = 0, charBox, currentColor;
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this.getHeightOfLine(i);
if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) {
lineTopOffset += heightOfLine;
continue;
}
line = this._textLines[i];
lineLeftOffset = this._getLineLeftOffset(i);
boxWidth = 0;
boxStart = 0;
lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
for (var j = 0, jlen = line.length; j < jlen; j++) {
charBox = this.__charBounds[i][j];
currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
if (currentColor !== lastColor) {
ctx.fillStyle = lastColor;
lastColor && ctx.fillRect(
leftOffset + lineLeftOffset + boxStart,
topOffset + lineTopOffset,
boxWidth,
heightOfLine / this.lineHeight
);
boxStart = charBox.left;
boxWidth = charBox.width;
lastColor = currentColor;
}
else {
boxWidth += charBox.kernedWidth;
}
}
if (currentColor) {
ctx.fillStyle = currentColor;
ctx.fillRect(
leftOffset + lineLeftOffset + boxStart,
topOffset + lineTopOffset,
boxWidth,
heightOfLine / this.lineHeight
);
}
lineTopOffset += heightOfLine;
}
ctx.fillStyle = originalFill;
// if there is text background color no
// other shadows should be casted
this._removeShadow(ctx);
},
/**
* @private
* @param {Object} decl style declaration for cache
* @param {String} decl.fontFamily fontFamily
* @param {String} decl.fontStyle fontStyle
* @param {String} decl.fontWeight fontWeight
* @return {Object} reference to cache
*/
getFontCache: function(decl) {
var fontFamily = decl.fontFamily.toLowerCase();
if (!fabric.charWidthsCache[fontFamily]) {
fabric.charWidthsCache[fontFamily] = { };
}
var cache = fabric.charWidthsCache[fontFamily],
cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase();
if (!cache[cacheProp]) {
cache[cacheProp] = { };
}
return cache[cacheProp];
},
/**
* apply all the character style to canvas for rendering
* @private
* @param {String} _char
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {Object} [decl]
*/
_applyCharStyles: function(method, ctx, lineIndex, charIndex, styleDeclaration) {
this._setFillStyles(ctx, styleDeclaration);
this._setStrokeStyles(ctx, styleDeclaration);
ctx.font = this._getFontDeclaration(styleDeclaration);
},
/**
* measure and return the width of a single character.
* possibly overridden to accommodate different measure logic or
* to hook some external lib for character measurement
* @private
* @param {String} _char, char to be measured
* @param {Object} charStyle style of char to be measured
* @param {String} [previousChar] previous char
* @param {Object} [prevCharStyle] style of previous char
*/
_measureChar: function(_char, charStyle, previousChar, prevCharStyle) {
// first i try to return from cache
var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle),
previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char,
stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth,
fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth;
if (previousChar && fontCache[previousChar] !== undefined) {
previousWidth = fontCache[previousChar];
}
if (fontCache[_char] !== undefined) {
kernedWidth = width = fontCache[_char];
}
if (stylesAreEqual && fontCache[couple] !== undefined) {
coupleWidth = fontCache[couple];
kernedWidth = coupleWidth - previousWidth;
}
if (width === undefined || previousWidth === undefined || coupleWidth === undefined) {
var ctx = this.getMeasuringContext();
// send a TRUE to specify measuring font size CACHE_FONT_SIZE
this._setTextStyles(ctx, charStyle, true);
}
if (width === undefined) {
kernedWidth = width = ctx.measureText(_char).width;
fontCache[_char] = width;
}
if (previousWidth === undefined && stylesAreEqual && previousChar) {
previousWidth = ctx.measureText(previousChar).width;
fontCache[previousChar] = previousWidth;
}
if (stylesAreEqual && coupleWidth === undefined) {
// we can measure the kerning couple and subtract the width of the previous character
coupleWidth = ctx.measureText(couple).width;
fontCache[couple] = coupleWidth;
kernedWidth = coupleWidth - previousWidth;
}
return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier };
},
/**
* Computes height of character at given position
* @param {Number} line the line index number
* @param {Number} _char the character index number
* @return {Number} fontSize of the character
*/
getHeightOfChar: function(line, _char) {
return this.getValueOfPropertyAt(line, _char, 'fontSize');
},
/**
* measure a text line measuring all characters.
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
measureLine: function(lineIndex) {
var lineInfo = this._measureLine(lineIndex);
if (this.charSpacing !== 0) {
lineInfo.width -= this._getWidthOfCharSpacing();
}
if (lineInfo.width < 0) {
lineInfo.width = 0;
}
return lineInfo;
},
/**
* measure every grapheme of a line, populating __charBounds
* @param {Number} lineIndex
* @return {Object} object.width total width of characters
* @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs
*/
_measureLine: function(lineIndex) {
var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme,
graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length);
this.__charBounds[lineIndex] = lineBounds;
for (i = 0; i < line.length; i++) {
grapheme = line[i];
graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme);
lineBounds[i] = graphemeInfo;
width += graphemeInfo.kernedWidth;
prevGrapheme = grapheme;
}
// this latest bound box represent the last character of the line
// to simplify cursor handling in interactive mode.
lineBounds[i] = {
left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0,
width: 0,
kernedWidth: 0,
height: this.fontSize
};
return { width: width, numOfSpaces: numOfSpaces };
},
/**
* Measure and return the info of a single grapheme.
* needs the the info of previous graphemes already filled
* @private
* @param {String} grapheme to be measured
* @param {Number} lineIndex index of the line where the char is
* @param {Number} charIndex position in the line
* @param {String} [prevGrapheme] character preceding the one to be measured
*/
_getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) {
var style = this.getCompleteStyleDeclaration(lineIndex, charIndex),
prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { },
info = this._measureChar(grapheme, style, prevGrapheme, prevStyle),
kernedWidth = info.kernedWidth,
width = info.width, charSpacing;
if (this.charSpacing !== 0) {
charSpacing = this._getWidthOfCharSpacing();
width += charSpacing;
kernedWidth += charSpacing;
}
var box = {
width: width,
left: 0,
height: style.fontSize,
kernedWidth: kernedWidth,
deltaY: style.deltaY,
};
if (charIndex > 0 && !skipLeft) {
var previousBox = this.__charBounds[lineIndex][charIndex - 1];
box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width;
}
return box;
},
/**
* Calculate height of line at 'lineIndex'
* @param {Number} lineIndex index of line to calculate
* @return {Number}
*/
getHeightOfLine: function(lineIndex) {
if (this.__lineHeights[lineIndex]) {
return this.__lineHeights[lineIndex];
}
var line = this._textLines[lineIndex],
// char 0 is measured before the line cycle because it nneds to char
// emptylines
maxHeight = this.getHeightOfChar(lineIndex, 0);
for (var i = 1, len = line.length; i < len; i++) {
maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight);
}
return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;
},
/**
* Calculate text box height
*/
calcTextHeight: function() {
var lineHeight, height = 0;
for (var i = 0, len = this._textLines.length; i < len; i++) {
lineHeight = this.getHeightOfLine(i);
height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight);
}
return height;
},
/**
* @private
* @return {Number} Left offset
*/
_getLeftOffset: function() {
return -this.width / 2;
},
/**
* @private
* @return {Number} Top offset
*/
_getTopOffset: function() {
return -this.height / 2;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} method Method name ("fillText" or "strokeText")
*/
_renderTextCommon: function(ctx, method) {
ctx.save();
var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(),
offsets = this._applyPatternGradientTransform(ctx, method === 'fillText' ? this.fill : this.stroke);
for (var i = 0, len = this._textLines.length; i < len; i++) {
var heightOfLine = this.getHeightOfLine(i),
maxHeight = heightOfLine / this.lineHeight,
leftOffset = this._getLineLeftOffset(i);
this._renderTextLine(
method,
ctx,
this._textLines[i],
left + leftOffset - offsets.offsetX,
top + lineHeights + maxHeight - offsets.offsetY,
i
);
lineHeights += heightOfLine;
}
ctx.restore();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextFill: function(ctx) {
if (!this.fill && !this.styleHas('fill')) {
return;
}
this._renderTextCommon(ctx, 'fillText');
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextStroke: function(ctx) {
if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) {
return;
}
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
ctx.save();
this._setLineDash(ctx, this.strokeDashArray);
ctx.beginPath();
this._renderTextCommon(ctx, 'strokeText');
ctx.closePath();
ctx.restore();
},
/**
* @private
* @param {String} method
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line Content of the line
* @param {Number} left
* @param {Number} top
* @param {Number} lineIndex
* @param {Number} charOffset
*/
_renderChars: function(method, ctx, line, left, top, lineIndex) {
// set proper line offset
var lineHeight = this.getHeightOfLine(lineIndex),
isJustify = this.textAlign.indexOf('justify') !== -1,
actualStyle,
nextStyle,
charsToRender = '',
charBox,
boxWidth = 0,
timeToRender,
shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex);
ctx.save();
top -= lineHeight * this._fontSizeFraction / this.lineHeight;
if (shortCut) {
// render all the line in one pass without checking
this._renderChar(method, ctx, lineIndex, 0, this.textLines[lineIndex], left, top, lineHeight);
ctx.restore();
return;
}
for (var i = 0, len = line.length - 1; i <= len; i++) {
timeToRender = i === len || this.charSpacing;
charsToRender += line[i];
charBox = this.__charBounds[lineIndex][i];
if (boxWidth === 0) {
left += charBox.kernedWidth - charBox.width;
boxWidth += charBox.width;
}
else {
boxWidth += charBox.kernedWidth;
}
if (isJustify && !timeToRender) {
if (this._reSpaceAndTab.test(line[i])) {
timeToRender = true;
}
}
if (!timeToRender) {
// if we have charSpacing, we render char by char
actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
}
if (timeToRender) {
this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight);
charsToRender = '';
actualStyle = nextStyle;
left += boxWidth;
boxWidth = 0;
}
}
ctx.restore();
},
/**
* @private
* @param {String} method
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {String} _char
* @param {Number} left Left coordinate
* @param {Number} top Top coordinate
* @param {Number} lineHeight Height of the line
*/
_renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) {
var decl = this._getStyleDeclaration(lineIndex, charIndex),
fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex),
shouldFill = method === 'fillText' && fullDecl.fill,
shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth;
if (!shouldStroke && !shouldFill) {
return;
}
decl && ctx.save();
this._applyCharStyles(method, ctx, lineIndex, charIndex, fullDecl);
if (decl && decl.textBackgroundColor) {
this._removeShadow(ctx);
}
if (decl && decl.deltaY) {
top += decl.deltaY;
}
shouldFill && ctx.fillText(_char, left, top);
shouldStroke && ctx.strokeText(_char, left, top);
decl && ctx.restore();
},
/**
* Turns the character into a 'superior figure' (i.e. 'superscript')
* @param {Number} start selection start
* @param {Number} end selection end
* @returns {fabric.Text} thisArg
* @chainable
*/
setSuperscript: function(start, end) {
return this._setScript(start, end, this.superscript);
},
/**
* Turns the character into an 'inferior figure' (i.e. 'subscript')
* @param {Number} start selection start
* @param {Number} end selection end
* @returns {fabric.Text} thisArg
* @chainable
*/
setSubscript: function(start, end) {
return this._setScript(start, end, this.subscript);
},
/**
* Applies 'schema' at given position
* @private
* @param {Number} start selection start
* @param {Number} end selection end
* @param {Number} schema
* @returns {fabric.Text} thisArg
* @chainable
*/
_setScript: function(start, end, schema) {
var loc = this.get2DCursorLocation(start, true),
fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'),
dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'),
style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline };
this.setSelectionStyles(style, start, end);
return this;
},
/**
* @private
* @param {Object} prevStyle
* @param {Object} thisStyle
*/
_hasStyleChanged: function(prevStyle, thisStyle) {
return prevStyle.fill !== thisStyle.fill ||
prevStyle.stroke !== thisStyle.stroke ||
prevStyle.strokeWidth !== thisStyle.strokeWidth ||
prevStyle.fontSize !== thisStyle.fontSize ||
prevStyle.fontFamily !== thisStyle.fontFamily ||
prevStyle.fontWeight !== thisStyle.fontWeight ||
prevStyle.fontStyle !== thisStyle.fontStyle ||
prevStyle.deltaY !== thisStyle.deltaY;
},
/**
* @private
* @param {Object} prevStyle
* @param {Object} thisStyle
*/
_hasStyleChangedForSvg: function(prevStyle, thisStyle) {
return this._hasStyleChanged(prevStyle, thisStyle) ||
prevStyle.overline !== thisStyle.overline ||
prevStyle.underline !== thisStyle.underline ||
prevStyle.linethrough !== thisStyle.linethrough;
},
/**
* @private
* @param {Number} lineIndex index text line
* @return {Number} Line left offset
*/
_getLineLeftOffset: function(lineIndex) {
var lineWidth = this.getLineWidth(lineIndex);
if (this.textAlign === 'center') {
return (this.width - lineWidth) / 2;
}
if (this.textAlign === 'right') {
return this.width - lineWidth;
}
if (this.textAlign === 'justify-center' && this.isEndOfWrapping(lineIndex)) {
return (this.width - lineWidth) / 2;
}
if (this.textAlign === 'justify-right' && this.isEndOfWrapping(lineIndex)) {
return this.width - lineWidth;
}
return 0;
},
/**
* @private
*/
_clearCache: function() {
this.__lineWidths = [];
this.__lineHeights = [];
this.__charBounds = [];
},
/**
* @private
*/
_shouldClearDimensionCache: function() {
var shouldClear = this._forceClearCache;
shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps'));
if (shouldClear) {
this.dirty = true;
this._forceClearCache = false;
}
return shouldClear;
},
/**
* Measure a single line given its index. Used to calculate the initial
* text bounding box. The values are calculated and stored in __lineWidths cache.
* @private
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
getLineWidth: function(lineIndex) {
if (this.__lineWidths[lineIndex]) {
return this.__lineWidths[lineIndex];
}
var width, line = this._textLines[lineIndex], lineInfo;
if (line === '') {
width = 0;
}
else {
lineInfo = this.measureLine(lineIndex);
width = lineInfo.width;
}
this.__lineWidths[lineIndex] = width;
return width;
},
_getWidthOfCharSpacing: function() {
if (this.charSpacing !== 0) {
return this.fontSize * this.charSpacing / 1000;
}
return 0;
},
/**
* Retrieves the value of property at given character position
* @param {Number} lineIndex the line number
* @param {Number} charIndex the charater number
* @param {String} property the property name
* @returns the value of 'property'
*/
getValueOfPropertyAt: function(lineIndex, charIndex, property) {
var charStyle = this._getStyleDeclaration(lineIndex, charIndex);
if (charStyle && typeof charStyle[property] !== 'undefined') {
return charStyle[property];
}
return this[property];
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextDecoration: function(ctx, type) {
if (!this[type] && !this.styleHas(type)) {
return;
}
var heightOfLine, size, _size,
lineLeftOffset, dy, _dy,
line, lastDecoration,
leftOffset = this._getLeftOffset(),
topOffset = this._getTopOffset(), top,
boxStart, boxWidth, charBox, currentDecoration,
maxHeight, currentFill, lastFill,
charSpacing = this._getWidthOfCharSpacing();
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this.getHeightOfLine(i);
if (!this[type] && !this.styleHas(type, i)) {
topOffset += heightOfLine;
continue;
}
line = this._textLines[i];
maxHeight = heightOfLine / this.lineHeight;
lineLeftOffset = this._getLineLeftOffset(i);
boxStart = 0;
boxWidth = 0;
lastDecoration = this.getValueOfPropertyAt(i, 0, type);
lastFill = this.getValueOfPropertyAt(i, 0, 'fill');
top = topOffset + maxHeight * (1 - this._fontSizeFraction);
size = this.getHeightOfChar(i, 0);
dy = this.getValueOfPropertyAt(i, 0, 'deltaY');
for (var j = 0, jlen = line.length; j < jlen; j++) {
charBox = this.__charBounds[i][j];
currentDecoration = this.getValueOfPropertyAt(i, j, type);
currentFill = this.getValueOfPropertyAt(i, j, 'fill');
_size = this.getHeightOfChar(i, j);
_dy = this.getValueOfPropertyAt(i, j, 'deltaY');
if ((currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) &&
boxWidth > 0) {
ctx.fillStyle = lastFill;
lastDecoration && lastFill && ctx.fillRect(
leftOffset + lineLeftOffset + boxStart,
top + this.offsets[type] * size + dy,
boxWidth,
this.fontSize / 15
);
boxStart = charBox.left;
boxWidth = charBox.width;
lastDecoration = currentDecoration;
lastFill = currentFill;
size = _size;
dy = _dy;
}
else {
boxWidth += charBox.kernedWidth;
}
}
ctx.fillStyle = currentFill;
currentDecoration && currentFill && ctx.fillRect(
leftOffset + lineLeftOffset + boxStart,
top + this.offsets[type] * size + dy,
boxWidth - charSpacing,
this.fontSize / 15
);
topOffset += heightOfLine;
}
// if there is text background color no
// other shadows should be casted
this._removeShadow(ctx);
},
/**
* return font declaration string for canvas context
* @param {Object} [styleObject] object
* @returns {String} font declaration formatted for canvas context.
*/
_getFontDeclaration: function(styleObject, forMeasuring) {
var style = styleObject || this, family = this.fontFamily,
fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1;
var fontFamily = family === undefined ||
family.indexOf('\'') > -1 || family.indexOf(',') > -1 ||
family.indexOf('"') > -1 || fontIsGeneric
? style.fontFamily : '"' + style.fontFamily + '"';
return [
// node-canvas needs "weight style", while browsers need "style weight"
// verify if this can be fixed in JSDOM
(fabric.isLikelyNode ? style.fontWeight : style.fontStyle),
(fabric.isLikelyNode ? style.fontStyle : style.fontWeight),
forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px',
fontFamily
].join(' ');
},
/**
* Renders text instance on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
render: function(ctx) {
// do not render if object is not visible
if (!this.visible) {
return;
}
if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {
return;
}
if (this._shouldClearDimensionCache()) {
this.initDimensions();
}
this.callSuper('render', ctx);
},
/**
* Returns the text as an array of lines.
* @param {String} text text to split
* @returns {Array} Lines in the text
*/
_splitTextIntoLines: function(text) {
var lines = text.split(this._reNewline),
newLines = new Array(lines.length),
newLine = ['\n'],
newText = [];
for (var i = 0; i < lines.length; i++) {
newLines[i] = fabric.util.string.graphemeSplit(lines[i]);
newText = newText.concat(newLines[i], newLine);
}
newText.pop();
return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines };
},
/**
* Returns object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toObject: function(propertiesToInclude) {
var additionalProperties = [
'text',
'fontSize',
'fontWeight',
'fontFamily',
'fontStyle',
'lineHeight',
'underline',
'overline',
'linethrough',
'textAlign',
'textBackgroundColor',
'charSpacing',
].concat(propertiesToInclude);
var obj = this.callSuper('toObject', additionalProperties);
obj.styles = clone(this.styles, true);
return obj;
},
/**
* Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
* @param {String|Object} key Property name or object (if object, iterate over the object properties)
* @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
* @return {fabric.Object} thisArg
* @chainable
*/
set: function(key, value) {
this.callSuper('set', key, value);
var needsDims = false;
if (typeof key === 'object') {
for (var _key in key) {
needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1;
}
}
else {
needsDims = this._dimensionAffectingProps.indexOf(key) !== -1;
}
if (needsDims) {
this.initDimensions();
this.setCoords();
}
return this;
},
/**
* Returns complexity of an instance
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement})
* @static
* @memberOf fabric.Text
* @see: http://www.w3.org/TR/SVG/text.html#TextElement
*/
fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(
'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' '));
/**
* Default SVG font size
* @static
* @memberOf fabric.Text
*/
fabric.Text.DEFAULT_SVG_FONT_SIZE = 16;
/**
* Returns fabric.Text instance from an SVG element (not yet implemented)
* @static
* @memberOf fabric.Text
* @param {SVGElement} element Element to parse
* @param {Function} callback callback function invoked after parsing
* @param {Object} [options] Options object
*/
fabric.Text.fromElement = function(element, callback, options) {
if (!element) {
return callback(null);
}
var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES),
parsedAnchor = parsedAttributes.textAnchor || 'left';
options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes);
options.top = options.top || 0;
options.left = options.left || 0;
if (parsedAttributes.textDecoration) {
var textDecoration = parsedAttributes.textDecoration;
if (textDecoration.indexOf('underline') !== -1) {
options.underline = true;
}
if (textDecoration.indexOf('overline') !== -1) {
options.overline = true;
}
if (textDecoration.indexOf('line-through') !== -1) {
options.linethrough = true;
}
delete options.textDecoration;
}
if ('dx' in parsedAttributes) {
options.left += parsedAttributes.dx;
}
if ('dy' in parsedAttributes) {
options.top += parsedAttributes.dy;
}
if (!('fontSize' in options)) {
options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
}
var textContent = '';
// The XML is not properly parsed in IE9 so a workaround to get
// textContent is through firstChild.data. Another workaround would be
// to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does)
if (!('textContent' in element)) {
if ('firstChild' in element && element.firstChild !== null) {
if ('data' in element.firstChild && element.firstChild.data !== null) {
textContent = element.firstChild.data;
}
}
}
else {
textContent = element.textContent;
}
textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' ');
var originalStrokeWidth = options.strokeWidth;
options.strokeWidth = 0;
var text = new fabric.Text(textContent, options),
textHeightScaleFactor = text.getScaledHeight() / text.height,
lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height,
scaledDiff = lineHeightDiff * textHeightScaleFactor,
textHeight = text.getScaledHeight() + scaledDiff,
offX = 0;
/*
Adjust positioning:
x/y attributes in SVG correspond to the bottom-left corner of text bounding box
fabric output by default at top, left.
*/
if (parsedAnchor === 'center') {
offX = text.getScaledWidth() / 2;
}
if (parsedAnchor === 'right') {
offX = text.getScaledWidth();
}
text.set({
left: text.left - offX,
top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight,
strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1,
});
callback(text);
};
/* _FROM_SVG_END_ */
/**
* Returns fabric.Text instance from an object representation
* @static
* @memberOf fabric.Text
* @param {Object} object Object to create an instance from
* @param {Function} [callback] Callback to invoke when an fabric.Text instance is created
*/
fabric.Text.fromObject = function(object, callback) {
return fabric.Object._fromObject('Text', object, callback, 'text');
};
fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace'];
fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text);
})(typeof exports !== 'undefined' ? exports : this);
(function() {
fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
/**
* Returns true if object has no styling or no styling in a line
* @param {Number} lineIndex , lineIndex is on wrapped lines.
* @return {Boolean}
*/
isEmptyStyles: function(lineIndex) {
if (!this.styles) {
return true;
}
if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
return true;
}
var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
for (var p1 in obj) {
for (var p2 in obj[p1]) {
// eslint-disable-next-line no-unused-vars
for (var p3 in obj[p1][p2]) {
return false;
}
}
}
return true;
},
/**
* Returns true if object has a style property or has it ina specified line
* This function is used to detect if a text will use a particular property or not.
* @param {String} property to check for
* @param {Number} lineIndex to check the style on
* @return {Boolean}
*/
styleHas: function(property, lineIndex) {
if (!this.styles || !property || property === '') {
return false;
}
if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
return false;
}
var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] };
// eslint-disable-next-line
for (var p1 in obj) {
// eslint-disable-next-line
for (var p2 in obj[p1]) {
if (typeof obj[p1][p2][property] !== 'undefined') {
return true;
}
}
}
return false;
},
/**
* Check if characters in a text have a value for a property
* whose value matches the textbox's value for that property. If so,
* the character-level property is deleted. If the character
* has no other properties, then it is also deleted. Finally,
* if the line containing that character has no other characters
* then it also is deleted.
*
* @param {string} property The property to compare between characters and text.
*/
cleanStyle: function(property) {
if (!this.styles || !property || property === '') {
return false;
}
var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue,
allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject;
// eslint-disable-next-line
for (var p1 in obj) {
letterCount = 0;
// eslint-disable-next-line
for (var p2 in obj[p1]) {
var styleObject = obj[p1][p2],
stylePropertyHasBeenSet = styleObject.hasOwnProperty(property);
stylesCount++;
if (stylePropertyHasBeenSet) {
if (!stylePropertyValue) {
stylePropertyValue = styleObject[property];
}
else if (styleObject[property] !== stylePropertyValue) {
allStyleObjectPropertiesMatch = false;
}
if (styleObject[property] === this[property]) {
delete styleObject[property];
}
}
else {
allStyleObjectPropertiesMatch = false;
}
if (Object.keys(styleObject).length !== 0) {
letterCount++;
}
else {
delete obj[p1][p2];
}
}
if (letterCount === 0) {
delete obj[p1];
}
}
// if every grapheme has the same style set then
// delete those styles and set it on the parent
for (var i = 0; i < this._textLines.length; i++) {
graphemeCount += this._textLines[i].length;
}
if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) {
this[property] = stylePropertyValue;
this.removeStyle(property);
}
},
/**
* Remove a style property or properties from all individual character styles
* in a text object. Deletes the character style object if it contains no other style
* props. Deletes a line style object if it contains no other character styles.
*
* @param {String} props The property to remove from character styles.
*/
removeStyle: function(property) {
if (!this.styles || !property || property === '') {
return;
}
var obj = this.styles, line, lineNum, charNum;
for (lineNum in obj) {
line = obj[lineNum];
for (charNum in line) {
delete line[charNum][property];
if (Object.keys(line[charNum]).length === 0) {
delete line[charNum];
}
}
if (Object.keys(line).length === 0) {
delete obj[lineNum];
}
}
},
/**
* @private
*/
_extendStyles: function(index, styles) {
var loc = this.get2DCursorLocation(index);
if (!this._getLineStyle(loc.lineIndex)) {
this._setLineStyle(loc.lineIndex);
}
if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) {
this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {});
}
fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles);
},
/**
* Returns 2d representation (lineIndex and charIndex) of cursor (or selection start)
* @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
* @param {Boolean} [skipWrapping] consider the location for unwrapped lines. usefull to manage styles.
*/
get2DCursorLocation: function(selectionStart, skipWrapping) {
if (typeof selectionStart === 'undefined') {
selectionStart = this.selectionStart;
}
var lines = skipWrapping ? this._unwrappedTextLines : this._textLines,
len = lines.length;
for (var i = 0; i < len; i++) {
if (selectionStart <= lines[i].length) {
return {
lineIndex: i,
charIndex: selectionStart
};
}
selectionStart -= lines[i].length + this.missingNewlineOffset(i);
}
return {
lineIndex: i - 1,
charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart
};
},
/**
* Gets style of a current selection/cursor (at the start position)
* if startIndex or endIndex are not provided, slectionStart or selectionEnd will be used.
* @param {Number} [startIndex] Start index to get styles at
* @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
* @param {Boolean} [complete] get full style or not
* @return {Array} styles an array with one, zero or more Style objects
*/
getSelectionStyles: function(startIndex, endIndex, complete) {
if (typeof startIndex === 'undefined') {
startIndex = this.selectionStart || 0;
}
if (typeof endIndex === 'undefined') {
endIndex = this.selectionEnd || startIndex;
}
var styles = [];
for (var i = startIndex; i < endIndex; i++) {
styles.push(this.getStyleAtPosition(i, complete));
}
return styles;
},
/**
* Gets style of a current selection/cursor position
* @param {Number} position to get styles at
* @param {Boolean} [complete] full style if true
* @return {Object} style Style object at a specified index
* @private
*/
getStyleAtPosition: function(position, complete) {
var loc = this.get2DCursorLocation(position),
style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) :
this._getStyleDeclaration(loc.lineIndex, loc.charIndex);
return style || {};
},
/**
* Sets style of a current selection, if no selection exist, do not set anything.
* @param {Object} [styles] Styles object
* @param {Number} [startIndex] Start index to get styles at
* @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
* @return {fabric.IText} thisArg
* @chainable
*/
setSelectionStyles: function(styles, startIndex, endIndex) {
if (typeof startIndex === 'undefined') {
startIndex = this.selectionStart || 0;
}
if (typeof endIndex === 'undefined') {
endIndex = this.selectionEnd || startIndex;
}
for (var i = startIndex; i < endIndex; i++) {
this._extendStyles(i, styles);
}
/* not included in _extendStyles to avoid clearing cache more than once */
this._forceClearCache = true;
return this;
},
/**
* get the reference, not a clone, of the style object for a given character
* @param {Number} lineIndex
* @param {Number} charIndex
* @return {Object} style object
*/
_getStyleDeclaration: function(lineIndex, charIndex) {
var lineStyle = this.styles && this.styles[lineIndex];
if (!lineStyle) {
return null;
}
return lineStyle[charIndex];
},
/**
* return a new object that contains all the style property for a character
* the object returned is newly created
* @param {Number} lineIndex of the line where the character is
* @param {Number} charIndex position of the character on the line
* @return {Object} style object
*/
getCompleteStyleDeclaration: function(lineIndex, charIndex) {
var style = this._getStyleDeclaration(lineIndex, charIndex) || { },
styleObject = { }, prop;
for (var i = 0; i < this._styleProperties.length; i++) {
prop = this._styleProperties[i];
styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop];
}
return styleObject;
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {Object} style
* @private
*/
_setStyleDeclaration: function(lineIndex, charIndex, style) {
this.styles[lineIndex][charIndex] = style;
},
/**
*
* @param {Number} lineIndex
* @param {Number} charIndex
* @private
*/
_deleteStyleDeclaration: function(lineIndex, charIndex) {
delete this.styles[lineIndex][charIndex];
},
/**
* @param {Number} lineIndex
* @return {Boolean} if the line exists or not
* @private
*/
_getLineStyle: function(lineIndex) {
return !!this.styles[lineIndex];
},
/**
* Set the line style to an empty object so that is initialized
* @param {Number} lineIndex
* @private
*/
_setLineStyle: function(lineIndex) {
this.styles[lineIndex] = {};
},
/**
* @param {Number} lineIndex
* @private
*/
_deleteLineStyle: function(lineIndex) {
delete this.styles[lineIndex];
}
});
})();
(function() {
function parseDecoration(object) {
if (object.textDecoration) {
object.textDecoration.indexOf('underline') > -1 && (object.underline = true);
object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true);
object.textDecoration.indexOf('overline') > -1 && (object.overline = true);
delete object.textDecoration;
}
}
/**
* IText class (introduced in v1.4) Events are also fired with "text:"
* prefix when observing canvas.
* @class fabric.IText
* @extends fabric.Text
* @mixes fabric.Observable
*
* @fires changed
* @fires selection:changed
* @fires editing:entered
* @fires editing:exited
*
* @return {fabric.IText} thisArg
* @see {@link fabric.IText#initialize} for constructor definition
*
* Supported key combinations:
*
* Move cursor: left, right, up, down
* Select character: shift + left, shift + right
* Select text vertically: shift + up, shift + down
* Move cursor by word: alt + left, alt + right
* Select words: shift + alt + left, shift + alt + right
* Move cursor to line start/end: cmd + left, cmd + right or home, end
* Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end
* Jump to start/end of text: cmd + up, cmd + down
* Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
* Delete character: backspace
* Delete word: alt + backspace
* Delete line: cmd + backspace
* Forward delete: delete
* Copy text: ctrl/cmd + c
* Paste text: ctrl/cmd + v
* Cut text: ctrl/cmd + x
* Select entire text: ctrl/cmd + a
* Quit editing tab or esc
*
*
* Supported mouse/touch combination
*
* Position cursor: click/touch
* Create selection: click/touch & drag
* Create selection: click & shift + click
* Select word: double click
* Select line: triple click
*
*/
fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'i-text',
/**
* Index where text selection starts (or where cursor is when there is no selection)
* @type Number
* @default
*/
selectionStart: 0,
/**
* Index where text selection ends
* @type Number
* @default
*/
selectionEnd: 0,
/**
* Color of text selection
* @type String
* @default
*/
selectionColor: 'rgba(17,119,255,0.3)',
/**
* Indicates whether text is in editing mode
* @type Boolean
* @default
*/
isEditing: false,
/**
* Indicates whether a text can be edited
* @type Boolean
* @default
*/
editable: true,
/**
* Border color of text object while it's in editing mode
* @type String
* @default
*/
editingBorderColor: 'rgba(102,153,255,0.25)',
/**
* Width of cursor (in px)
* @type Number
* @default
*/
cursorWidth: 2,
/**
* Color of default cursor (when not overwritten by character style)
* @type String
* @default
*/
cursorColor: '#333',
/**
* Delay between cursor blink (in ms)
* @type Number
* @default
*/
cursorDelay: 1000,
/**
* Duration of cursor fadein (in ms)
* @type Number
* @default
*/
cursorDuration: 600,
/**
* Indicates whether internal text char widths can be cached
* @type Boolean
* @default
*/
caching: true,
/**
* @private
*/
_reSpace: /\s|\n/,
/**
* @private
*/
_currentCursorOpacity: 0,
/**
* @private
*/
_selectionDirection: null,
/**
* @private
*/
_abortCursorAnimation: false,
/**
* @private
*/
__widthOfSpace: [],
/**
* Helps determining when the text is in composition, so that the cursor
* rendering is altered.
*/
inCompositionMode: false,
/**
* Constructor
* @param {String} text Text string
* @param {Object} [options] Options object
* @return {fabric.IText} thisArg
*/
initialize: function(text, options) {
this.callSuper('initialize', text, options);
this.initBehavior();
},
/**
* Sets selection start (left boundary of a selection)
* @param {Number} index Index to set selection start to
*/
setSelectionStart: function(index) {
index = Math.max(index, 0);
this._updateAndFire('selectionStart', index);
},
/**
* Sets selection end (right boundary of a selection)
* @param {Number} index Index to set selection end to
*/
setSelectionEnd: function(index) {
index = Math.min(index, this.text.length);
this._updateAndFire('selectionEnd', index);
},
/**
* @private
* @param {String} property 'selectionStart' or 'selectionEnd'
* @param {Number} index new position of property
*/
_updateAndFire: function(property, index) {
if (this[property] !== index) {
this._fireSelectionChanged();
this[property] = index;
}
this._updateTextarea();
},
/**
* Fires the even of selection changed
* @private
*/
_fireSelectionChanged: function() {
this.fire('selection:changed');
this.canvas && this.canvas.fire('text:selection:changed', { target: this });
},
/**
* Initialize text dimensions. Render all text on given context
* or on a offscreen canvas to get the text width with measureText.
* Updates this.width and this.height with the proper values.
* Does not return dimensions.
* @private
*/
initDimensions: function() {
this.isEditing && this.initDelayedCursor();
this.clearContextTop();
this.callSuper('initDimensions');
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
render: function(ctx) {
this.clearContextTop();
this.callSuper('render', ctx);
// clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
// the correct position but not at every cursor animation.
this.cursorOffsetCache = { };
this.renderCursorOrSelection();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
this.callSuper('_render', ctx);
},
/**
* Prepare and clean the contextTop
*/
clearContextTop: function(skipRestore) {
if (!this.isEditing || !this.canvas || !this.canvas.contextTop) {
return;
}
var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform;
ctx.save();
ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
this.transform(ctx);
this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);
this._clearTextArea(ctx);
skipRestore || ctx.restore();
},
/**
* Renders cursor or selection (depending on what exists)
* it does on the contextTop. If contextTop is not available, do nothing.
*/
renderCursorOrSelection: function() {
if (!this.isEditing || !this.canvas || !this.canvas.contextTop) {
return;
}
var boundaries = this._getCursorBoundaries(),
ctx = this.canvas.contextTop;
this.clearContextTop(true);
if (this.selectionStart === this.selectionEnd) {
this.renderCursor(boundaries, ctx);
}
else {
this.renderSelection(boundaries, ctx);
}
ctx.restore();
},
_clearTextArea: function(ctx) {
// we add 4 pixel, to be sure to do not leave any pixel out
var width = this.width + 4, height = this.height + 4;
ctx.clearRect(-width / 2, -height / 2, width, height);
},
/**
* Returns cursor boundaries (left, top, leftOffset, topOffset)
* @private
* @param {Array} chars Array of characters
* @param {String} typeOfBoundaries
*/
_getCursorBoundaries: function(position) {
// left/top are left/top of entire text box
// leftOffset/topOffset are offset from that left/top point of a text box
if (typeof position === 'undefined') {
position = this.selectionStart;
}
var left = this._getLeftOffset(),
top = this._getTopOffset(),
offsets = this._getCursorBoundariesOffsets(position);
return {
left: left,
top: top,
leftOffset: offsets.left,
topOffset: offsets.top
};
},
/**
* @private
*/
_getCursorBoundariesOffsets: function(position) {
if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
return this.cursorOffsetCache;
}
var lineLeftOffset,
lineIndex,
charIndex,
topOffset = 0,
leftOffset = 0,
boundaries,
cursorPosition = this.get2DCursorLocation(position);
charIndex = cursorPosition.charIndex;
lineIndex = cursorPosition.lineIndex;
for (var i = 0; i < lineIndex; i++) {
topOffset += this.getHeightOfLine(i);
}
lineLeftOffset = this._getLineLeftOffset(lineIndex);
var bound = this.__charBounds[lineIndex][charIndex];
bound && (leftOffset = bound.left);
if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
leftOffset -= this._getWidthOfCharSpacing();
}
boundaries = {
top: topOffset,
left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0),
};
this.cursorOffsetCache = boundaries;
return this.cursorOffsetCache;
},
/**
* Renders cursor
* @param {Object} boundaries
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
*/
renderCursor: function(boundaries, ctx) {
var cursorLocation = this.get2DCursorLocation(),
lineIndex = cursorLocation.lineIndex,
charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0,
charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'),
multiplier = this.scaleX * this.canvas.getZoom(),
cursorWidth = this.cursorWidth / multiplier,
topOffset = boundaries.topOffset,
dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY');
topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight
- charHeight * (1 - this._fontSizeFraction);
if (this.inCompositionMode) {
this.renderSelection(boundaries, ctx);
}
ctx.fillStyle = this.getValueOfPropertyAt(lineIndex, charIndex, 'fill');
ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
ctx.fillRect(
boundaries.left + boundaries.leftOffset - cursorWidth / 2,
topOffset + boundaries.top + dy,
cursorWidth,
charHeight);
},
/**
* Renders text selection
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
*/
renderSelection: function(boundaries, ctx) {
var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd,
isJustify = this.textAlign.indexOf('justify') !== -1,
start = this.get2DCursorLocation(selectionStart),
end = this.get2DCursorLocation(selectionEnd),
startLine = start.lineIndex,
endLine = end.lineIndex,
startChar = start.charIndex < 0 ? 0 : start.charIndex,
endChar = end.charIndex < 0 ? 0 : end.charIndex;
for (var i = startLine; i <= endLine; i++) {
var lineOffset = this._getLineLeftOffset(i) || 0,
lineHeight = this.getHeightOfLine(i),
realLineHeight = 0, boxStart = 0, boxEnd = 0;
if (i === startLine) {
boxStart = this.__charBounds[startLine][startChar].left;
}
if (i >= startLine && i < endLine) {
boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5?
}
else if (i === endLine) {
if (endChar === 0) {
boxEnd = this.__charBounds[endLine][endChar].left;
}
else {
var charSpacing = this._getWidthOfCharSpacing();
boxEnd = this.__charBounds[endLine][endChar - 1].left
+ this.__charBounds[endLine][endChar - 1].width - charSpacing;
}
}
realLineHeight = lineHeight;
if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {
lineHeight /= this.lineHeight;
}
if (this.inCompositionMode) {
ctx.fillStyle = this.compositionColor || 'black';
ctx.fillRect(
boundaries.left + lineOffset + boxStart,
boundaries.top + boundaries.topOffset + lineHeight,
boxEnd - boxStart,
1);
}
else {
ctx.fillStyle = this.selectionColor;
ctx.fillRect(
boundaries.left + lineOffset + boxStart,
boundaries.top + boundaries.topOffset,
boxEnd - boxStart,
lineHeight);
}
boundaries.topOffset += realLineHeight;
}
},
/**
* High level function to know the height of the cursor.
* the currentChar is the one that precedes the cursor
* Returns fontSize of char at the current cursor
* @return {Number} Character font size
*/
getCurrentCharFontSize: function() {
var cp = this._getCurrentCharIndex();
return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize');
},
/**
* High level function to know the color of the cursor.
* the currentChar is the one that precedes the cursor
* Returns color (fill) of char at the current cursor
* @return {String} Character color (fill)
*/
getCurrentCharColor: function() {
var cp = this._getCurrentCharIndex();
return this.getValueOfPropertyAt(cp.l, cp.c, 'fill');
},
/**
* Returns the cursor position for the getCurrent.. functions
* @private
*/
_getCurrentCharIndex: function() {
var cursorPosition = this.get2DCursorLocation(this.selectionStart, true),
charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0;
return { l: cursorPosition.lineIndex, c: charIndex };
}
});
/**
* Returns fabric.IText instance from an object representation
* @static
* @memberOf fabric.IText
* @param {Object} object Object to create an instance from
* @param {function} [callback] invoked with new instance as argument
*/
fabric.IText.fromObject = function(object, callback) {
parseDecoration(object);
if (object.styles) {
for (var i in object.styles) {
for (var j in object.styles[i]) {
parseDecoration(object.styles[i][j]);
}
}
}
fabric.Object._fromObject('IText', object, callback, 'text');
};
})();
(function() {
var clone = fabric.util.object.clone;
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
/**
* Initializes all the interactive behavior of IText
*/
initBehavior: function() {
this.initAddedHandler();
this.initRemovedHandler();
this.initCursorSelectionHandlers();
this.initDoubleClickSimulation();
this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
},
onDeselect: function() {
this.isEditing && this.exitEditing();
this.selected = false;
},
/**
* Initializes "added" event handler
*/
initAddedHandler: function() {
var _this = this;
this.on('added', function() {
var canvas = _this.canvas;
if (canvas) {
if (!canvas._hasITextHandlers) {
canvas._hasITextHandlers = true;
_this._initCanvasHandlers(canvas);
}
canvas._iTextInstances = canvas._iTextInstances || [];
canvas._iTextInstances.push(_this);
}
});
},
initRemovedHandler: function() {
var _this = this;
this.on('removed', function() {
var canvas = _this.canvas;
if (canvas) {
canvas._iTextInstances = canvas._iTextInstances || [];
fabric.util.removeFromArray(canvas._iTextInstances, _this);
if (canvas._iTextInstances.length === 0) {
canvas._hasITextHandlers = false;
_this._removeCanvasHandlers(canvas);
}
}
});
},
/**
* register canvas event to manage exiting on other instances
* @private
*/
_initCanvasHandlers: function(canvas) {
canvas._mouseUpITextHandler = function() {
if (canvas._iTextInstances) {
canvas._iTextInstances.forEach(function(obj) {
obj.__isMousedown = false;
});
}
};
canvas.on('mouse:up', canvas._mouseUpITextHandler);
},
/**
* remove canvas event to manage exiting on other instances
* @private
*/
_removeCanvasHandlers: function(canvas) {
canvas.off('mouse:up', canvas._mouseUpITextHandler);
},
/**
* @private
*/
_tick: function() {
this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete');
},
/**
* @private
*/
_animateCursor: function(obj, targetOpacity, duration, completeMethod) {
var tickState;
tickState = {
isAborted: false,
abort: function() {
this.isAborted = true;
},
};
obj.animate('_currentCursorOpacity', targetOpacity, {
duration: duration,
onComplete: function() {
if (!tickState.isAborted) {
obj[completeMethod]();
}
},
onChange: function() {
// we do not want to animate a selection, only cursor
if (obj.canvas && obj.selectionStart === obj.selectionEnd) {
obj.renderCursorOrSelection();
}
},
abort: function() {
return tickState.isAborted;
}
});
return tickState;
},
/**
* @private
*/
_onTickComplete: function() {
var _this = this;
if (this._cursorTimeout1) {
clearTimeout(this._cursorTimeout1);
}
this._cursorTimeout1 = setTimeout(function() {
_this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick');
}, 100);
},
/**
* Initializes delayed cursor
*/
initDelayedCursor: function(restart) {
var _this = this,
delay = restart ? 0 : this.cursorDelay;
this.abortCursorAnimation();
this._currentCursorOpacity = 1;
this._cursorTimeout2 = setTimeout(function() {
_this._tick();
}, delay);
},
/**
* Aborts cursor animation and clears all timeouts
*/
abortCursorAnimation: function() {
var shouldClear = this._currentTickState || this._currentTickCompleteState,
canvas = this.canvas;
this._currentTickState && this._currentTickState.abort();
this._currentTickCompleteState && this._currentTickCompleteState.abort();
clearTimeout(this._cursorTimeout1);
clearTimeout(this._cursorTimeout2);
this._currentCursorOpacity = 0;
// to clear just itext area we need to transform the context
// it may not be worth it
if (shouldClear && canvas) {
canvas.clearContext(canvas.contextTop || canvas.contextContainer);
}
},
/**
* Selects entire text
* @return {fabric.IText} thisArg
* @chainable
*/
selectAll: function() {
this.selectionStart = 0;
this.selectionEnd = this._text.length;
this._fireSelectionChanged();
this._updateTextarea();
return this;
},
/**
* Returns selected text
* @return {String}
*/
getSelectedText: function() {
return this._text.slice(this.selectionStart, this.selectionEnd).join('');
},
/**
* Find new selection index representing start of current word according to current selection index
* @param {Number} startFrom Current selection index
* @return {Number} New selection index
*/
findWordBoundaryLeft: function(startFrom) {
var offset = 0, index = startFrom - 1;
// remove space before cursor first
if (this._reSpace.test(this._text[index])) {
while (this._reSpace.test(this._text[index])) {
offset++;
index--;
}
}
while (/\S/.test(this._text[index]) && index > -1) {
offset++;
index--;
}
return startFrom - offset;
},
/**
* Find new selection index representing end of current word according to current selection index
* @param {Number} startFrom Current selection index
* @return {Number} New selection index
*/
findWordBoundaryRight: function(startFrom) {
var offset = 0, index = startFrom;
// remove space after cursor first
if (this._reSpace.test(this._text[index])) {
while (this._reSpace.test(this._text[index])) {
offset++;
index++;
}
}
while (/\S/.test(this._text[index]) && index < this._text.length) {
offset++;
index++;
}
return startFrom + offset;
},
/**
* Find new selection index representing start of current line according to current selection index
* @param {Number} startFrom Current selection index
* @return {Number} New selection index
*/
findLineBoundaryLeft: function(startFrom) {
var offset = 0, index = startFrom - 1;
while (!/\n/.test(this._text[index]) && index > -1) {
offset++;
index--;
}
return startFrom - offset;
},
/**
* Find new selection index representing end of current line according to current selection index
* @param {Number} startFrom Current selection index
* @return {Number} New selection index
*/
findLineBoundaryRight: function(startFrom) {
var offset = 0, index = startFrom;
while (!/\n/.test(this._text[index]) && index < this._text.length) {
offset++;
index++;
}
return startFrom + offset;
},
/**
* Finds index corresponding to beginning or end of a word
* @param {Number} selectionStart Index of a character
* @param {Number} direction 1 or -1
* @return {Number} Index of the beginning or end of a word
*/
searchWordBoundary: function(selectionStart, direction) {
var index = this._reSpace.test(this._text[selectionStart]) ? selectionStart - 1 : selectionStart,
_char = this._text[index],
reNonWord = /[ \n\.,;!\?\-]/;
while (!reNonWord.test(_char) && index > 0 && index < this._text.length) {
index += direction;
_char = this._text[index];
}
if (reNonWord.test(_char) && _char !== '\n') {
index += direction === 1 ? 0 : 1;
}
return index;
},
/**
* Selects a word based on the index
* @param {Number} selectionStart Index of a character
*/
selectWord: function(selectionStart) {
selectionStart = selectionStart || this.selectionStart;
var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */
newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */
this.selectionStart = newSelectionStart;
this.selectionEnd = newSelectionEnd;
this._fireSelectionChanged();
this._updateTextarea();
this.renderCursorOrSelection();
},
/**
* Selects a line based on the index
* @param {Number} selectionStart Index of a character
* @return {fabric.IText} thisArg
* @chainable
*/
selectLine: function(selectionStart) {
selectionStart = selectionStart || this.selectionStart;
var newSelectionStart = this.findLineBoundaryLeft(selectionStart),
newSelectionEnd = this.findLineBoundaryRight(selectionStart);
this.selectionStart = newSelectionStart;
this.selectionEnd = newSelectionEnd;
this._fireSelectionChanged();
this._updateTextarea();
return this;
},
/**
* Enters editing state
* @return {fabric.IText} thisArg
* @chainable
*/
enterEditing: function(e) {
if (this.isEditing || !this.editable) {
return;
}
if (this.canvas) {
this.canvas.calcOffset();
this.exitEditingOnOthers(this.canvas);
}
this.isEditing = true;
this.initHiddenTextarea(e);
this.hiddenTextarea.focus();
this.hiddenTextarea.value = this.text;
this._updateTextarea();
this._saveEditingProps();
this._setEditingProps();
this._textBeforeEdit = this.text;
this._tick();
this.fire('editing:entered');
this._fireSelectionChanged();
if (!this.canvas) {
return this;
}
this.canvas.fire('text:editing:entered', { target: this });
this.initMouseMoveHandler();
this.canvas.requestRenderAll();
return this;
},
exitEditingOnOthers: function(canvas) {
if (canvas._iTextInstances) {
canvas._iTextInstances.forEach(function(obj) {
obj.selected = false;
if (obj.isEditing) {
obj.exitEditing();
}
});
}
},
/**
* Initializes "mousemove" event handler
*/
initMouseMoveHandler: function() {
this.canvas.on('mouse:move', this.mouseMoveHandler);
},
/**
* @private
*/
mouseMoveHandler: function(options) {
if (!this.__isMousedown || !this.isEditing) {
return;
}
var newSelectionStart = this.getSelectionStartFromPointer(options.e),
currentStart = this.selectionStart,
currentEnd = this.selectionEnd;
if (
(newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd)
&&
(currentStart === newSelectionStart || currentEnd === newSelectionStart)
) {
return;
}
if (newSelectionStart > this.__selectionStartOnMouseDown) {
this.selectionStart = this.__selectionStartOnMouseDown;
this.selectionEnd = newSelectionStart;
}
else {
this.selectionStart = newSelectionStart;
this.selectionEnd = this.__selectionStartOnMouseDown;
}
if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) {
this.restartCursorIfNeeded();
this._fireSelectionChanged();
this._updateTextarea();
this.renderCursorOrSelection();
}
},
/**
* @private
*/
_setEditingProps: function() {
this.hoverCursor = 'text';
if (this.canvas) {
this.canvas.defaultCursor = this.canvas.moveCursor = 'text';
}
this.borderColor = this.editingBorderColor;
this.hasControls = this.selectable = false;
this.lockMovementX = this.lockMovementY = true;
},
/**
* convert from textarea to grapheme indexes
*/
fromStringToGraphemeSelection: function(start, end, text) {
var smallerTextStart = text.slice(0, start),
graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length;
if (start === end) {
return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
}
var smallerTextEnd = text.slice(start, end),
graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length;
return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
},
/**
* convert from fabric to textarea values
*/
fromGraphemeToStringSelection: function(start, end, _text) {
var smallerTextStart = _text.slice(0, start),
graphemeStart = smallerTextStart.join('').length;
if (start === end) {
return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
}
var smallerTextEnd = _text.slice(start, end),
graphemeEnd = smallerTextEnd.join('').length;
return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
},
/**
* @private
*/
_updateTextarea: function() {
this.cursorOffsetCache = { };
if (!this.hiddenTextarea) {
return;
}
if (!this.inCompositionMode) {
var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text);
this.hiddenTextarea.selectionStart = newSelection.selectionStart;
this.hiddenTextarea.selectionEnd = newSelection.selectionEnd;
}
this.updateTextareaPosition();
},
/**
* @private
*/
updateFromTextArea: function() {
if (!this.hiddenTextarea) {
return;
}
this.cursorOffsetCache = { };
this.text = this.hiddenTextarea.value;
if (this._shouldClearDimensionCache()) {
this.initDimensions();
this.setCoords();
}
var newSelection = this.fromStringToGraphemeSelection(
this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value);
this.selectionEnd = this.selectionStart = newSelection.selectionEnd;
if (!this.inCompositionMode) {
this.selectionStart = newSelection.selectionStart;
}
this.updateTextareaPosition();
},
/**
* @private
*/
updateTextareaPosition: function() {
if (this.selectionStart === this.selectionEnd) {
var style = this._calcTextareaPosition();
this.hiddenTextarea.style.left = style.left;
this.hiddenTextarea.style.top = style.top;
}
},
/**
* @private
* @return {Object} style contains style for hiddenTextarea
*/
_calcTextareaPosition: function() {
if (!this.canvas) {
return { x: 1, y: 1 };
}
var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart,
boundaries = this._getCursorBoundaries(desiredPosition),
cursorLocation = this.get2DCursorLocation(desiredPosition),
lineIndex = cursorLocation.lineIndex,
charIndex = cursorLocation.charIndex,
charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight,
leftOffset = boundaries.leftOffset,
m = this.calcTransformMatrix(),
p = {
x: boundaries.left + leftOffset,
y: boundaries.top + boundaries.topOffset + charHeight
},
upperCanvas = this.canvas.upperCanvasEl,
upperCanvasWidth = upperCanvas.width,
upperCanvasHeight = upperCanvas.height,
maxWidth = upperCanvasWidth - charHeight,
maxHeight = upperCanvasHeight - charHeight,
scaleX = upperCanvas.clientWidth / upperCanvasWidth,
scaleY = upperCanvas.clientHeight / upperCanvasHeight;
p = fabric.util.transformPoint(p, m);
p = fabric.util.transformPoint(p, this.canvas.viewportTransform);
p.x *= scaleX;
p.y *= scaleY;
if (p.x < 0) {
p.x = 0;
}
if (p.x > maxWidth) {
p.x = maxWidth;
}
if (p.y < 0) {
p.y = 0;
}
if (p.y > maxHeight) {
p.y = maxHeight;
}
// add canvas offset on document
p.x += this.canvas._offset.left;
p.y += this.canvas._offset.top;
return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight };
},
/**
* @private
*/
_saveEditingProps: function() {
this._savedProps = {
hasControls: this.hasControls,
borderColor: this.borderColor,
lockMovementX: this.lockMovementX,
lockMovementY: this.lockMovementY,
hoverCursor: this.hoverCursor,
selectable: this.selectable,
defaultCursor: this.canvas && this.canvas.defaultCursor,
moveCursor: this.canvas && this.canvas.moveCursor
};
},
/**
* @private
*/
_restoreEditingProps: function() {
if (!this._savedProps) {
return;
}
this.hoverCursor = this._savedProps.hoverCursor;
this.hasControls = this._savedProps.hasControls;
this.borderColor = this._savedProps.borderColor;
this.selectable = this._savedProps.selectable;
this.lockMovementX = this._savedProps.lockMovementX;
this.lockMovementY = this._savedProps.lockMovementY;
if (this.canvas) {
this.canvas.defaultCursor = this._savedProps.defaultCursor;
this.canvas.moveCursor = this._savedProps.moveCursor;
}
},
/**
* Exits from editing state
* @return {fabric.IText} thisArg
* @chainable
*/
exitEditing: function() {
var isTextChanged = (this._textBeforeEdit !== this.text);
this.selected = false;
this.isEditing = false;
this.selectionEnd = this.selectionStart;
if (this.hiddenTextarea) {
this.hiddenTextarea.blur && this.hiddenTextarea.blur();
this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea);
this.hiddenTextarea = null;
}
this.abortCursorAnimation();
this._restoreEditingProps();
this._currentCursorOpacity = 0;
if (this._shouldClearDimensionCache()) {
this.initDimensions();
this.setCoords();
}
this.fire('editing:exited');
isTextChanged && this.fire('modified');
if (this.canvas) {
this.canvas.off('mouse:move', this.mouseMoveHandler);
this.canvas.fire('text:editing:exited', { target: this });
isTextChanged && this.canvas.fire('object:modified', { target: this });
}
return this;
},
/**
* @private
*/
_removeExtraneousStyles: function() {
for (var prop in this.styles) {
if (!this._textLines[prop]) {
delete this.styles[prop];
}
}
},
/**
* remove and reflow a style block from start to end.
* @param {Number} start linear start position for removal (included in removal)
* @param {Number} end linear end position for removal ( excluded from removal )
*/
removeStyleFromTo: function(start, end) {
var cursorStart = this.get2DCursorLocation(start, true),
cursorEnd = this.get2DCursorLocation(end, true),
lineStart = cursorStart.lineIndex,
charStart = cursorStart.charIndex,
lineEnd = cursorEnd.lineIndex,
charEnd = cursorEnd.charIndex,
i, styleObj;
if (lineStart !== lineEnd) {
// step1 remove the trailing of lineStart
if (this.styles[lineStart]) {
for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) {
delete this.styles[lineStart][i];
}
}
// step2 move the trailing of lineEnd to lineStart if needed
if (this.styles[lineEnd]) {
for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) {
styleObj = this.styles[lineEnd][i];
if (styleObj) {
this.styles[lineStart] || (this.styles[lineStart] = { });
this.styles[lineStart][charStart + i - charEnd] = styleObj;
}
}
}
// step3 detects lines will be completely removed.
for (i = lineStart + 1; i <= lineEnd; i++) {
delete this.styles[i];
}
// step4 shift remaining lines.
this.shiftLineStyles(lineEnd, lineStart - lineEnd);
}
else {
// remove and shift left on the same line
if (this.styles[lineStart]) {
styleObj = this.styles[lineStart];
var diff = charEnd - charStart, numericChar, _char;
for (i = charStart; i < charEnd; i++) {
delete styleObj[i];
}
for (_char in this.styles[lineStart]) {
numericChar = parseInt(_char, 10);
if (numericChar >= charEnd) {
styleObj[numericChar - diff] = styleObj[_char];
delete styleObj[_char];
}
}
}
}
},
/**
* Shifts line styles up or down
* @param {Number} lineIndex Index of a line
* @param {Number} offset Can any number?
*/
shiftLineStyles: function(lineIndex, offset) {
// shift all line styles by offset upward or downward
// do not clone deep. we need new array, not new style objects
var clonedStyles = clone(this.styles);
for (var line in this.styles) {
var numericLine = parseInt(line, 10);
if (numericLine > lineIndex) {
this.styles[numericLine + offset] = clonedStyles[numericLine];
if (!clonedStyles[numericLine - offset]) {
delete this.styles[numericLine];
}
}
}
},
restartCursorIfNeeded: function() {
if (!this._currentTickState || this._currentTickState.isAborted
|| !this._currentTickCompleteState || this._currentTickCompleteState.isAborted
) {
this.initDelayedCursor();
}
},
/**
* Inserts new style object
* @param {Number} lineIndex Index of a line
* @param {Number} charIndex Index of a char
* @param {Number} qty number of lines to add
* @param {Array} copiedStyle Array of objects styles
*/
insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) {
var currentCharStyle,
newLineStyles = {},
somethingAdded = false;
qty || (qty = 1);
this.shiftLineStyles(lineIndex, qty);
if (this.styles[lineIndex]) {
currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1];
}
// we clone styles of all chars
// after cursor onto the current line
for (var index in this.styles[lineIndex]) {
var numIndex = parseInt(index, 10);
if (numIndex >= charIndex) {
somethingAdded = true;
newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];
// remove lines from the previous line since they're on a new line now
delete this.styles[lineIndex][index];
}
}
if (somethingAdded) {
this.styles[lineIndex + qty] = newLineStyles;
}
else {
delete this.styles[lineIndex + qty];
}
// for the other lines
// we clone current char style onto the next (otherwise empty) line
while (qty > 1) {
qty--;
if (copiedStyle && copiedStyle[qty]) {
this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty]) };
}
else if (currentCharStyle) {
this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) };
}
else {
delete this.styles[lineIndex + qty];
}
}
this._forceClearCache = true;
},
/**
* Inserts style object for a given line/char index
* @param {Number} lineIndex Index of a line
* @param {Number} charIndex Index of a char
* @param {Number} quantity number Style object to insert, if given
* @param {Array} copiedStyle array of style objects
*/
insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) {
if (!this.styles) {
this.styles = {};
}
var currentLineStyles = this.styles[lineIndex],
currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {};
quantity || (quantity = 1);
// shift all char styles by quantity forward
// 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4
for (var index in currentLineStylesCloned) {
var numericIndex = parseInt(index, 10);
if (numericIndex >= charIndex) {
currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex];
// only delete the style if there was nothing moved there
if (!currentLineStylesCloned[numericIndex - quantity]) {
delete currentLineStyles[numericIndex];
}
}
}
this._forceClearCache = true;
if (copiedStyle) {
while (quantity--) {
if (!Object.keys(copiedStyle[quantity]).length) {
continue;
}
if (!this.styles[lineIndex]) {
this.styles[lineIndex] = {};
}
this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]);
}
return;
}
if (!currentLineStyles) {
return;
}
var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1];
while (newStyle && quantity--) {
this.styles[lineIndex][charIndex + quantity] = clone(newStyle);
}
},
/**
* Inserts style object(s)
* @param {Array} insertedText Characters at the location where style is inserted
* @param {Number} start cursor index for inserting style
* @param {Array} [copiedStyle] array of style objects to insert.
*/
insertNewStyleBlock: function(insertedText, start, copiedStyle) {
var cursorLoc = this.get2DCursorLocation(start, true),
addedLines = [0], linesLength = 0;
for (var i = 0; i < insertedText.length; i++) {
if (insertedText[i] === '\n') {
linesLength++;
addedLines[linesLength] = 0;
}
else {
addedLines[linesLength]++;
}
}
if (addedLines[0] > 0) {
this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle);
copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1);
}
linesLength && this.insertNewlineStyleObject(
cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength);
for (var i = 1; i < linesLength; i++) {
if (addedLines[i] > 0) {
this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
}
else if (copiedStyle) {
this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0];
}
copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1);
}
// we use i outside the loop to get it like linesLength
if (addedLines[i] > 0) {
this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
}
},
/**
* Set the selectionStart and selectionEnd according to the new position of cursor
* mimic the key - mouse navigation when shift is pressed.
*/
setSelectionStartEndWithShift: function(start, end, newSelection) {
if (newSelection <= start) {
if (end === start) {
this._selectionDirection = 'left';
}
else if (this._selectionDirection === 'right') {
this._selectionDirection = 'left';
this.selectionEnd = start;
}
this.selectionStart = newSelection;
}
else if (newSelection > start && newSelection < end) {
if (this._selectionDirection === 'right') {
this.selectionEnd = newSelection;
}
else {
this.selectionStart = newSelection;
}
}
else {
// newSelection is > selection start and end
if (end === start) {
this._selectionDirection = 'right';
}
else if (this._selectionDirection === 'left') {
this._selectionDirection = 'right';
this.selectionStart = end;
}
this.selectionEnd = newSelection;
}
},
setSelectionInBoundaries: function() {
var length = this.text.length;
if (this.selectionStart > length) {
this.selectionStart = length;
}
else if (this.selectionStart < 0) {
this.selectionStart = 0;
}
if (this.selectionEnd > length) {
this.selectionEnd = length;
}
else if (this.selectionEnd < 0) {
this.selectionEnd = 0;
}
}
});
})();
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
/**
* Initializes "dbclick" event handler
*/
initDoubleClickSimulation: function() {
// for double click
this.__lastClickTime = +new Date();
// for triple click
this.__lastLastClickTime = +new Date();
this.__lastPointer = { };
this.on('mousedown', this.onMouseDown);
},
/**
* Default event handler to simulate triple click
* @private
*/
onMouseDown: function(options) {
if (!this.canvas) {
return;
}
this.__newClickTime = +new Date();
var newPointer = options.pointer;
if (this.isTripleClick(newPointer)) {
this.fire('tripleclick', options);
this._stopEvent(options.e);
}
this.__lastLastClickTime = this.__lastClickTime;
this.__lastClickTime = this.__newClickTime;
this.__lastPointer = newPointer;
this.__lastIsEditing = this.isEditing;
this.__lastSelected = this.selected;
},
isTripleClick: function(newPointer) {
return this.__newClickTime - this.__lastClickTime < 500 &&
this.__lastClickTime - this.__lastLastClickTime < 500 &&
this.__lastPointer.x === newPointer.x &&
this.__lastPointer.y === newPointer.y;
},
/**
* @private
*/
_stopEvent: function(e) {
e.preventDefault && e.preventDefault();
e.stopPropagation && e.stopPropagation();
},
/**
* Initializes event handlers related to cursor or selection
*/
initCursorSelectionHandlers: function() {
this.initMousedownHandler();
this.initMouseupHandler();
this.initClicks();
},
/**
* Initializes double and triple click event handlers
*/
initClicks: function() {
this.on('mousedblclick', function(options) {
this.selectWord(this.getSelectionStartFromPointer(options.e));
});
this.on('tripleclick', function(options) {
this.selectLine(this.getSelectionStartFromPointer(options.e));
});
},
/**
* Default event handler for the basic functionalities needed on _mouseDown
* can be overridden to do something different.
* Scope of this implementation is: find the click position, set selectionStart
* find selectionEnd, initialize the drawing of either cursor or selection area
*/
_mouseDownHandler: function(options) {
if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
return;
}
this.__isMousedown = true;
if (this.selected) {
this.setCursorByClick(options.e);
}
if (this.isEditing) {
this.__selectionStartOnMouseDown = this.selectionStart;
if (this.selectionStart === this.selectionEnd) {
this.abortCursorAnimation();
}
this.renderCursorOrSelection();
}
},
/**
* Default event handler for the basic functionalities needed on mousedown:before
* can be overridden to do something different.
* Scope of this implementation is: verify the object is already selected when mousing down
*/
_mouseDownHandlerBefore: function(options) {
if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
return;
}
if (this === this.canvas._activeObject) {
this.selected = true;
}
},
/**
* Initializes "mousedown" event handler
*/
initMousedownHandler: function() {
this.on('mousedown', this._mouseDownHandler);
this.on('mousedown:before', this._mouseDownHandlerBefore);
},
/**
* Initializes "mouseup" event handler
*/
initMouseupHandler: function() {
this.on('mouseup', this.mouseUpHandler);
},
/**
* standard hander for mouse up, overridable
* @private
*/
mouseUpHandler: function(options) {
this.__isMousedown = false;
if (!this.editable || this.group ||
(options.transform && options.transform.actionPerformed) ||
(options.e.button && options.e.button !== 1)) {
return;
}
if (this.canvas) {
var currentActive = this.canvas._activeObject;
if (currentActive && currentActive !== this) {
// avoid running this logic when there is an active object
// this because is possible with shift click and fast clicks,
// to rapidly deselect and reselect this object and trigger an enterEdit
return;
}
}
if (this.__lastSelected && !this.__corner) {
this.selected = false;
this.__lastSelected = false;
this.enterEditing(options.e);
if (this.selectionStart === this.selectionEnd) {
this.initDelayedCursor(true);
}
else {
this.renderCursorOrSelection();
}
}
else {
this.selected = true;
}
},
/**
* Changes cursor location in a text depending on passed pointer (x/y) object
* @param {Event} e Event object
*/
setCursorByClick: function(e) {
var newSelection = this.getSelectionStartFromPointer(e),
start = this.selectionStart, end = this.selectionEnd;
if (e.shiftKey) {
this.setSelectionStartEndWithShift(start, end, newSelection);
}
else {
this.selectionStart = newSelection;
this.selectionEnd = newSelection;
}
if (this.isEditing) {
this._fireSelectionChanged();
this._updateTextarea();
}
},
/**
* Returns index of a character corresponding to where an object was clicked
* @param {Event} e Event object
* @return {Number} Index of a character
*/
getSelectionStartFromPointer: function(e) {
var mouseOffset = this.getLocalPointer(e),
prevWidth = 0,
width = 0,
height = 0,
charIndex = 0,
lineIndex = 0,
lineLeftOffset,
line;
for (var i = 0, len = this._textLines.length; i < len; i++) {
if (height <= mouseOffset.y) {
height += this.getHeightOfLine(i) * this.scaleY;
lineIndex = i;
if (i > 0) {
charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1);
}
}
else {
break;
}
}
lineLeftOffset = this._getLineLeftOffset(lineIndex);
width = lineLeftOffset * this.scaleX;
line = this._textLines[lineIndex];
for (var j = 0, jlen = line.length; j < jlen; j++) {
prevWidth = width;
// i removed something about flipX here, check.
width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX;
if (width <= mouseOffset.x) {
charIndex++;
}
else {
break;
}
}
return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen);
},
/**
* @private
*/
_getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
// we need Math.abs because when width is after the last char, the offset is given as 1, while is 0
var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
distanceBtwNextCharAndCursor = width - mouseOffset.x,
offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ||
distanceBtwNextCharAndCursor < 0 ? 0 : 1,
newSelectionStart = index + offset;
// if object is horizontally flipped, mirror cursor location from the end
if (this.flipX) {
newSelectionStart = jlen - newSelectionStart;
}
if (newSelectionStart > this._text.length) {
newSelectionStart = this._text.length;
}
return newSelectionStart;
}
});
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
/**
* Initializes hidden textarea (needed to bring up keyboard in iOS)
*/
initHiddenTextarea: function() {
this.hiddenTextarea = fabric.document.createElement('textarea');
this.hiddenTextarea.setAttribute('autocapitalize', 'off');
this.hiddenTextarea.setAttribute('autocorrect', 'off');
this.hiddenTextarea.setAttribute('autocomplete', 'off');
this.hiddenTextarea.setAttribute('spellcheck', 'false');
this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', '');
this.hiddenTextarea.setAttribute('wrap', 'off');
var style = this._calcTextareaPosition();
// line-height: 1px; was removed from the style to fix this:
// https://bugs.chromium.org/p/chromium/issues/detail?id=870966
this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top +
'; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' +
' paddingーtop: ' + style.fontSize + ';';
fabric.document.body.appendChild(this.hiddenTextarea);
fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this));
fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this));
if (!this._clickHandlerInitialized && this.canvas) {
fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this));
this._clickHandlerInitialized = true;
}
},
/**
* For functionalities on keyDown
* Map a special key to a function of the instance/prototype
* If you need different behaviour for ESC or TAB or arrows, you have to change
* this map setting the name of a function that you build on the fabric.Itext or
* your prototype.
* the map change will affect all Instances unless you need for only some text Instances
* in that case you have to clone this object and assign your Instance.
* this.keysMap = fabric.util.object.clone(this.keysMap);
* The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0]
*/
keysMap: {
9: 'exitEditing',
27: 'exitEditing',
33: 'moveCursorUp',
34: 'moveCursorDown',
35: 'moveCursorRight',
36: 'moveCursorLeft',
37: 'moveCursorLeft',
38: 'moveCursorUp',
39: 'moveCursorRight',
40: 'moveCursorDown',
},
/**
* For functionalities on keyUp + ctrl || cmd
*/
ctrlKeysMapUp: {
67: 'copy',
88: 'cut'
},
/**
* For functionalities on keyDown + ctrl || cmd
*/
ctrlKeysMapDown: {
65: 'selectAll'
},
onClick: function() {
// No need to trigger click event here, focus is enough to have the keyboard appear on Android
this.hiddenTextarea && this.hiddenTextarea.focus();
},
/**
* Handles keyup event
* @param {Event} e Event object
*/
onKeyDown: function(e) {
if (!this.isEditing || this.inCompositionMode) {
return;
}
if (e.keyCode in this.keysMap) {
this[this.keysMap[e.keyCode]](e);
}
else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
this[this.ctrlKeysMapDown[e.keyCode]](e);
}
else {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
if (e.keyCode >= 33 && e.keyCode <= 40) {
// if i press an arrow key just update selection
this.clearContextTop();
this.renderCursorOrSelection();
}
else {
this.canvas && this.canvas.requestRenderAll();
}
},
/**
* Handles keyup event
* We handle KeyUp because ie11 and edge have difficulties copy/pasting
* if a copy/cut event fired, keyup is dismissed
* @param {Event} e Event object
*/
onKeyUp: function(e) {
if (!this.isEditing || this._copyDone || this.inCompositionMode) {
this._copyDone = false;
return;
}
if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
this[this.ctrlKeysMapUp[e.keyCode]](e);
}
else {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
this.canvas && this.canvas.requestRenderAll();
},
/**
* Handles onInput event
* @param {Event} e Event object
*/
onInput: function(e) {
var fromPaste = this.fromPaste;
this.fromPaste = false;
e && e.stopPropagation();
if (!this.isEditing) {
return;
}
// decisions about style changes.
var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,
charCount = this._text.length,
nextCharCount = nextText.length,
removedText, insertedText,
charDiff = nextCharCount - charCount;
if (this.hiddenTextarea.value === '') {
this.styles = { };
this.updateFromTextArea();
this.fire('changed');
if (this.canvas) {
this.canvas.fire('text:changed', { target: this });
this.canvas.requestRenderAll();
}
return;
}
var textareaSelection = this.fromStringToGraphemeSelection(
this.hiddenTextarea.selectionStart,
this.hiddenTextarea.selectionEnd,
this.hiddenTextarea.value
);
var backDelete = this.selectionStart > textareaSelection.selectionStart;
if (this.selectionStart !== this.selectionEnd) {
removedText = this._text.slice(this.selectionStart, this.selectionEnd);
charDiff += this.selectionEnd - this.selectionStart;
}
else if (nextCharCount < charCount) {
if (backDelete) {
removedText = this._text.slice(this.selectionEnd + charDiff, this.selectionEnd);
}
else {
removedText = this._text.slice(this.selectionStart, this.selectionStart - charDiff);
}
}
insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
if (removedText && removedText.length) {
if (this.selectionStart !== this.selectionEnd) {
this.removeStyleFromTo(this.selectionStart, this.selectionEnd);
}
else if (backDelete) {
// detect differencies between forwardDelete and backDelete
this.removeStyleFromTo(this.selectionEnd - removedText.length, this.selectionEnd);
}
else {
this.removeStyleFromTo(this.selectionEnd, this.selectionEnd + removedText.length);
}
}
if (insertedText.length) {
if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) {
this.insertNewStyleBlock(insertedText, this.selectionStart, fabric.copiedTextStyle);
}
else {
this.insertNewStyleBlock(insertedText, this.selectionStart);
}
}
this.updateFromTextArea();
this.fire('changed');
if (this.canvas) {
this.canvas.fire('text:changed', { target: this });
this.canvas.requestRenderAll();
}
},
/**
* Composition start
*/
onCompositionStart: function() {
this.inCompositionMode = true;
},
/**
* Composition end
*/
onCompositionEnd: function() {
this.inCompositionMode = false;
},
// /**
// * Composition update
// */
onCompositionUpdate: function(e) {
this.compositionStart = e.target.selectionStart;
this.compositionEnd = e.target.selectionEnd;
this.updateTextareaPosition();
},
/**
* Copies selected text
* @param {Event} e Event object
*/
copy: function() {
if (this.selectionStart === this.selectionEnd) {
//do not cut-copy if no selection
return;
}
fabric.copiedText = this.getSelectedText();
if (!fabric.disableStyleCopyPaste) {
fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true);
}
else {
fabric.copiedTextStyle = null;
}
this._copyDone = true;
},
/**
* Pastes text
* @param {Event} e Event object
*/
paste: function() {
this.fromPaste = true;
},
/**
* @private
* @param {Event} e Event object
* @return {Object} Clipboard data object
*/
_getClipboardData: function(e) {
return (e && e.clipboardData) || fabric.window.clipboardData;
},
/**
* Finds the width in pixels before the cursor on the same line
* @private
* @param {Number} lineIndex
* @param {Number} charIndex
* @return {Number} widthBeforeCursor width before cursor
*/
_getWidthBeforeCursor: function(lineIndex, charIndex) {
var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound;
if (charIndex > 0) {
bound = this.__charBounds[lineIndex][charIndex - 1];
widthBeforeCursor += bound.left + bound.width;
}
return widthBeforeCursor;
},
/**
* Gets start offset of a selection
* @param {Event} e Event object
* @param {Boolean} isRight
* @return {Number}
*/
getDownCursorOffset: function(e, isRight) {
var selectionProp = this._getSelectionForOffset(e, isRight),
cursorLocation = this.get2DCursorLocation(selectionProp),
lineIndex = cursorLocation.lineIndex;
// if on last line, down cursor goes to end of line
if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
// move to the end of a text
return this._text.length - selectionProp;
}
var charIndex = cursorLocation.charIndex,
widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),
textAfterCursor = this._textLines[lineIndex].slice(charIndex);
return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex);
},
/**
* private
* Helps finding if the offset should be counted from Start or End
* @param {Event} e Event object
* @param {Boolean} isRight
* @return {Number}
*/
_getSelectionForOffset: function(e, isRight) {
if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {
return this.selectionEnd;
}
else {
return this.selectionStart;
}
},
/**
* @param {Event} e Event object
* @param {Boolean} isRight
* @return {Number}
*/
getUpCursorOffset: function(e, isRight) {
var selectionProp = this._getSelectionForOffset(e, isRight),
cursorLocation = this.get2DCursorLocation(selectionProp),
lineIndex = cursorLocation.lineIndex;
if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
// if on first line, up cursor goes to start of line
return -selectionProp;
}
var charIndex = cursorLocation.charIndex,
widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor),
textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex),
missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1);
// return a negative offset
return -this._textLines[lineIndex - 1].length
+ indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset);
},
/**
* for a given width it founds the matching character.
* @private
*/
_getIndexOnLine: function(lineIndex, width) {
var line = this._textLines[lineIndex],
lineLeftOffset = this._getLineLeftOffset(lineIndex),
widthOfCharsOnLine = lineLeftOffset,
indexOnLine = 0, charWidth, foundMatch;
for (var j = 0, jlen = line.length; j < jlen; j++) {
charWidth = this.__charBounds[lineIndex][j].width;
widthOfCharsOnLine += charWidth;
if (widthOfCharsOnLine > width) {
foundMatch = true;
var leftEdge = widthOfCharsOnLine - charWidth,
rightEdge = widthOfCharsOnLine,
offsetFromLeftEdge = Math.abs(leftEdge - width),
offsetFromRightEdge = Math.abs(rightEdge - width);
indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
break;
}
}
// reached end
if (!foundMatch) {
indexOnLine = line.length - 1;
}
return indexOnLine;
},
/**
* Moves cursor down
* @param {Event} e Event object
*/
moveCursorDown: function(e) {
if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
return;
}
this._moveCursorUpOrDown('Down', e);
},
/**
* Moves cursor up
* @param {Event} e Event object
*/
moveCursorUp: function(e) {
if (this.selectionStart === 0 && this.selectionEnd === 0) {
return;
}
this._moveCursorUpOrDown('Up', e);
},
/**
* Moves cursor up or down, fires the events
* @param {String} direction 'Up' or 'Down'
* @param {Event} e Event object
*/
_moveCursorUpOrDown: function(direction, e) {
// getUpCursorOffset
// getDownCursorOffset
var action = 'get' + direction + 'CursorOffset',
offset = this[action](e, this._selectionDirection === 'right');
if (e.shiftKey) {
this.moveCursorWithShift(offset);
}
else {
this.moveCursorWithoutShift(offset);
}
if (offset !== 0) {
this.setSelectionInBoundaries();
this.abortCursorAnimation();
this._currentCursorOpacity = 1;
this.initDelayedCursor();
this._fireSelectionChanged();
this._updateTextarea();
}
},
/**
* Moves cursor with shift
* @param {Number} offset
*/
moveCursorWithShift: function(offset) {
var newSelection = this._selectionDirection === 'left'
? this.selectionStart + offset
: this.selectionEnd + offset;
this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection);
return offset !== 0;
},
/**
* Moves cursor up without shift
* @param {Number} offset
*/
moveCursorWithoutShift: function(offset) {
if (offset < 0) {
this.selectionStart += offset;
this.selectionEnd = this.selectionStart;
}
else {
this.selectionEnd += offset;
this.selectionStart = this.selectionEnd;
}
return offset !== 0;
},
/**
* Moves cursor left
* @param {Event} e Event object
*/
moveCursorLeft: function(e) {
if (this.selectionStart === 0 && this.selectionEnd === 0) {
return;
}
this._moveCursorLeftOrRight('Left', e);
},
/**
* @private
* @return {Boolean} true if a change happened
*/
_move: function(e, prop, direction) {
var newValue;
if (e.altKey) {
newValue = this['findWordBoundary' + direction](this[prop]);
}
else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) {
newValue = this['findLineBoundary' + direction](this[prop]);
}
else {
this[prop] += direction === 'Left' ? -1 : 1;
return true;
}
if (typeof newValue !== undefined && this[prop] !== newValue) {
this[prop] = newValue;
return true;
}
},
/**
* @private
*/
_moveLeft: function(e, prop) {
return this._move(e, prop, 'Left');
},
/**
* @private
*/
_moveRight: function(e, prop) {
return this._move(e, prop, 'Right');
},
/**
* Moves cursor left without keeping selection
* @param {Event} e
*/
moveCursorLeftWithoutShift: function(e) {
var change = true;
this._selectionDirection = 'left';
// only move cursor when there is no selection,
// otherwise we discard it, and leave cursor on same place
if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) {
change = this._moveLeft(e, 'selectionStart');
}
this.selectionEnd = this.selectionStart;
return change;
},
/**
* Moves cursor left while keeping selection
* @param {Event} e
*/
moveCursorLeftWithShift: function(e) {
if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {
return this._moveLeft(e, 'selectionEnd');
}
else if (this.selectionStart !== 0){
this._selectionDirection = 'left';
return this._moveLeft(e, 'selectionStart');
}
},
/**
* Moves cursor right
* @param {Event} e Event object
*/
moveCursorRight: function(e) {
if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
return;
}
this._moveCursorLeftOrRight('Right', e);
},
/**
* Moves cursor right or Left, fires event
* @param {String} direction 'Left', 'Right'
* @param {Event} e Event object
*/
_moveCursorLeftOrRight: function(direction, e) {
var actionName = 'moveCursor' + direction + 'With';
this._currentCursorOpacity = 1;
if (e.shiftKey) {
actionName += 'Shift';
}
else {
actionName += 'outShift';
}
if (this[actionName](e)) {
this.abortCursorAnimation();
this.initDelayedCursor();
this._fireSelectionChanged();
this._updateTextarea();
}
},
/**
* Moves cursor right while keeping selection
* @param {Event} e
*/
moveCursorRightWithShift: function(e) {
if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
return this._moveRight(e, 'selectionStart');
}
else if (this.selectionEnd !== this._text.length) {
this._selectionDirection = 'right';
return this._moveRight(e, 'selectionEnd');
}
},
/**
* Moves cursor right without keeping selection
* @param {Event} e Event object
*/
moveCursorRightWithoutShift: function(e) {
var changed = true;
this._selectionDirection = 'right';
if (this.selectionStart === this.selectionEnd) {
changed = this._moveRight(e, 'selectionStart');
this.selectionEnd = this.selectionStart;
}
else {
this.selectionStart = this.selectionEnd;
}
return changed;
},
/**
* Removes characters from start/end
* start/end ar per grapheme position in _text array.
*
* @param {Number} start
* @param {Number} end default to start + 1
*/
removeChars: function(start, end) {
if (typeof end === 'undefined') {
end = start + 1;
}
this.removeStyleFromTo(start, end);
this._text.splice(start, end - start);
this.text = this._text.join('');
this.set('dirty', true);
if (this._shouldClearDimensionCache()) {
this.initDimensions();
this.setCoords();
}
this._removeExtraneousStyles();
},
/**
* insert characters at start position, before start position.
* start equal 1 it means the text get inserted between actual grapheme 0 and 1
* if style array is provided, it must be as the same length of text in graphemes
* if end is provided and is bigger than start, old text is replaced.
* start/end ar per grapheme position in _text array.
*
* @param {String} text text to insert
* @param {Array} style array of style objects
* @param {Number} start
* @param {Number} end default to start + 1
*/
insertChars: function(text, style, start, end) {
if (typeof end === 'undefined') {
end = start;
}
if (end > start) {
this.removeStyleFromTo(start, end);
}
var graphemes = fabric.util.string.graphemeSplit(text);
this.insertNewStyleBlock(graphemes, start, style);
this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end));
this.text = this._text.join('');
this.set('dirty', true);
if (this._shouldClearDimensionCache()) {
this.initDimensions();
this.setCoords();
}
this._removeExtraneousStyles();
},
});
/* _TO_SVG_START_ */
(function() {
var toFixed = fabric.util.toFixed,
multipleSpacesRegex = / +/g;
fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
/**
* Returns SVG representation of an instance
* @param {Function} [reviver] Method for further parsing of svg representation.
* @return {String} svg representation of an instance
*/
_toSVG: function() {
var offsets = this._getSVGLeftTopOffsets(),
textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
return this._wrapSVGTextAndBg(textAndBg);
},
/**
* Returns svg representation of an instance
* @param {Function} [reviver] Method for further parsing of svg representation.
* @return {String} svg representation of an instance
*/
toSVG: function(reviver) {
return this._createBaseSVGMarkup(
this._toSVG(),
{ reviver: reviver, noStyle: true, withShadow: true }
);
},
/**
* @private
*/
_getSVGLeftTopOffsets: function() {
return {
textLeft: -this.width / 2,
textTop: -this.height / 2,
lineTop: this.getHeightOfLine(0)
};
},
/**
* @private
*/
_wrapSVGTextAndBg: function(textAndBg) {
var noShadow = true,
textDecoration = this.getSvgTextDecoration(this);
return [
textAndBg.textBgRects.join(''),
'\t\t',
textAndBg.textSpans.join(''),
'\n'
];
},
/**
* @private
* @param {Number} textTopOffset Text top offset
* @param {Number} textLeftOffset Text left offset
* @return {Object}
*/
_getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
var textSpans = [],
textBgRects = [],
height = textTopOffset, lineOffset;
// bounding-box background
this._setSVGBg(textBgRects);
// text and text-background
for (var i = 0, len = this._textLines.length; i < len; i++) {
lineOffset = this._getLineLeftOffset(i);
if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) {
this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
}
this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
height += this.getHeightOfLine(i);
}
return {
textSpans: textSpans,
textBgRects: textBgRects
};
},
/**
* @private
*/
_createTextCharSpan: function(_char, styleDecl, left, top) {
var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex),
styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace),
fillStyles = styleProps ? 'style="' + styleProps + '"' : '',
dy = styleDecl.deltaY, dySpan = '',
NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
if (dy) {
dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" ';
}
return [
'',
fabric.util.string.escapeXml(_char),
''
].join('');
},
_setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) {
// set proper line offset
var lineHeight = this.getHeightOfLine(lineIndex),
isJustify = this.textAlign.indexOf('justify') !== -1,
actualStyle,
nextStyle,
charsToRender = '',
charBox, style,
boxWidth = 0,
line = this._textLines[lineIndex],
timeToRender;
textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight;
for (var i = 0, len = line.length - 1; i <= len; i++) {
timeToRender = i === len || this.charSpacing;
charsToRender += line[i];
charBox = this.__charBounds[lineIndex][i];
if (boxWidth === 0) {
textLeftOffset += charBox.kernedWidth - charBox.width;
boxWidth += charBox.width;
}
else {
boxWidth += charBox.kernedWidth;
}
if (isJustify && !timeToRender) {
if (this._reSpaceAndTab.test(line[i])) {
timeToRender = true;
}
}
if (!timeToRender) {
// if we have charSpacing, we render char by char
actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle);
}
if (timeToRender) {
style = this._getStyleDeclaration(lineIndex, i) || { };
textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset));
charsToRender = '';
actualStyle = nextStyle;
textLeftOffset += boxWidth;
boxWidth = 0;
}
}
},
_pushTextBgRect: function(textBgRects, color, left, top, width, height) {
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
textBgRects.push(
'\t\t\n');
},
_setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) {
var line = this._textLines[i],
heightOfLine = this.getHeightOfLine(i) / this.lineHeight,
boxWidth = 0,
boxStart = 0,
charBox, currentColor,
lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
for (var j = 0, jlen = line.length; j < jlen; j++) {
charBox = this.__charBounds[i][j];
currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
if (currentColor !== lastColor) {
lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart,
textTopOffset, boxWidth, heightOfLine);
boxStart = charBox.left;
boxWidth = charBox.width;
lastColor = currentColor;
}
else {
boxWidth += charBox.kernedWidth;
}
}
currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart,
textTopOffset, boxWidth, heightOfLine);
},
/**
* Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values
* we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1
*
* @private
* @param {*} value
* @return {String}
*/
_getFillAttributes: function(value) {
var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : '';
if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {
return 'fill="' + value + '"';
}
return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"';
},
/**
* @private
*/
_getSVGLineTopOffset: function(lineIndex) {
var lineTopOffset = 0, lastHeight = 0;
for (var j = 0; j < lineIndex; j++) {
lineTopOffset += this.getHeightOfLine(j);
}
lastHeight = this.getHeightOfLine(j);
return {
lineTop: lineTopOffset,
offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
};
},
/**
* Returns styles-string for svg-export
* @param {Boolean} skipShadow a boolean to skip shadow filter output
* @return {String}
*/
getSvgStyles: function(skipShadow) {
var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow);
return svgStyle + ' white-space: pre;';
},
});
})();
/* _TO_SVG_END_ */
(function(global) {
'use strict';
var fabric = global.fabric || (global.fabric = {});
/**
* Textbox class, based on IText, allows the user to resize the text rectangle
* and wraps lines automatically. Textboxes have their Y scaling locked, the
* user can only change width. Height is adjusted automatically based on the
* wrapping of lines.
* @class fabric.Textbox
* @extends fabric.IText
* @mixes fabric.Observable
* @return {fabric.Textbox} thisArg
* @see {@link fabric.Textbox#initialize} for constructor definition
*/
fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {
/**
* Type of an object
* @type String
* @default
*/
type: 'textbox',
/**
* Minimum width of textbox, in pixels.
* @type Number
* @default
*/
minWidth: 20,
/**
* Minimum calculated width of a textbox, in pixels.
* fixed to 2 so that an empty textbox cannot go to 0
* and is still selectable without text.
* @type Number
* @default
*/
dynamicMinWidth: 2,
/**
* Cached array of text wrapping.
* @type Array
*/
__cachedLines: null,
/**
* Override standard Object class values
*/
lockScalingFlip: true,
/**
* Override standard Object class values
* Textbox needs this on false
*/
noScaleCache: false,
/**
* Properties which when set cause object to change dimensions
* @type Object
* @private
*/
_dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'),
/**
* Use this regular expression to split strings in breakable lines
* @private
*/
_wordJoiners: /[ \t\r]/,
/**
* Use this boolean property in order to split strings that have no white space concept.
* this is a cheap way to help with chinese/japaense
* @type Boolean
* @since 2.6.0
*/
splitByGrapheme: false,
/**
* Unlike superclass's version of this function, Textbox does not update
* its width.
* @private
* @override
*/
initDimensions: function() {
if (this.__skipDimension) {
return;
}
this.isEditing && this.initDelayedCursor();
this.clearContextTop();
this._clearCache();
// clear dynamicMinWidth as it will be different after we re-wrap line
this.dynamicMinWidth = 0;
// wrap lines
this._styleMap = this._generateStyleMap(this._splitText());
// if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
if (this.dynamicMinWidth > this.width) {
this._set('width', this.dynamicMinWidth);
}
if (this.textAlign.indexOf('justify') !== -1) {
// once text is measured we need to make space fatter to make justified text.
this.enlargeSpaces();
}
// clear cache and re-calculate height
this.height = this.calcTextHeight();
this.saveState({ propertySet: '_dimensionAffectingProps' });
},
/**
* Generate an object that translates the style object so that it is
* broken up by visual lines (new lines and automatic wrapping).
* The original text styles object is broken up by actual lines (new lines only),
* which is only sufficient for Text / IText
* @private
*/
_generateStyleMap: function(textInfo) {
var realLineCount = 0,
realLineCharCount = 0,
charCount = 0,
map = {};
for (var i = 0; i < textInfo.graphemeLines.length; i++) {
if (textInfo.graphemeText[charCount] === '\n' && i > 0) {
realLineCharCount = 0;
charCount++;
realLineCount++;
}
else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) {
// this case deals with space's that are removed from end of lines when wrapping
realLineCharCount++;
charCount++;
}
map[i] = { line: realLineCount, offset: realLineCharCount };
charCount += textInfo.graphemeLines[i].length;
realLineCharCount += textInfo.graphemeLines[i].length;
}
return map;
},
/**
* Returns true if object has a style property or has it on a specified line
* @param {Number} lineIndex
* @return {Boolean}
*/
styleHas: function(property, lineIndex) {
if (this._styleMap && !this.isWrapping) {
var map = this._styleMap[lineIndex];
if (map) {
lineIndex = map.line;
}
}
return fabric.Text.prototype.styleHas.call(this, property, lineIndex);
},
/**
* Returns true if object has no styling or no styling in a line
* @param {Number} lineIndex , lineIndex is on wrapped lines.
* @return {Boolean}
*/
isEmptyStyles: function(lineIndex) {
var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false;
var map = this._styleMap[lineIndex];
var mapNextLine = this._styleMap[lineIndex + 1];
if (map) {
lineIndex = map.line;
offset = map.offset;
}
if (mapNextLine) {
nextLineIndex = mapNextLine.line;
shouldLimit = nextLineIndex === lineIndex;
nextOffset = mapNextLine.offset;
}
obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
for (var p1 in obj) {
for (var p2 in obj[p1]) {
if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) {
// eslint-disable-next-line no-unused-vars
for (var p3 in obj[p1][p2]) {
return false;
}
}
}
}
return true;
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @private
*/
_getStyleDeclaration: function(lineIndex, charIndex) {
if (this._styleMap && !this.isWrapping) {
var map = this._styleMap[lineIndex];
if (!map) {
return null;
}
lineIndex = map.line;
charIndex = map.offset + charIndex;
}
return this.callSuper('_getStyleDeclaration', lineIndex, charIndex);
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {Object} style
* @private
*/
_setStyleDeclaration: function(lineIndex, charIndex, style) {
var map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
this.styles[lineIndex][charIndex] = style;
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @private
*/
_deleteStyleDeclaration: function(lineIndex, charIndex) {
var map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
delete this.styles[lineIndex][charIndex];
},
/**
* probably broken need a fix
* Returns the real style line that correspond to the wrapped lineIndex line
* Used just to verify if the line does exist or not.
* @param {Number} lineIndex
* @returns {Boolean} if the line exists or not
* @private
*/
_getLineStyle: function(lineIndex) {
var map = this._styleMap[lineIndex];
return !!this.styles[map.line];
},
/**
* Set the line style to an empty object so that is initialized
* @param {Number} lineIndex
* @param {Object} style
* @private
*/
_setLineStyle: function(lineIndex) {
var map = this._styleMap[lineIndex];
this.styles[map.line] = {};
},
/**
* Wraps text using the 'width' property of Textbox. First this function
* splits text on newlines, so we preserve newlines entered by the user.
* Then it wraps each line using the width of the Textbox by calling
* _wrapLine().
* @param {Array} lines The string array of text that is split into lines
* @param {Number} desiredWidth width you want to wrap to
* @returns {Array} Array of lines
*/
_wrapText: function(lines, desiredWidth) {
var wrapped = [], i;
this.isWrapping = true;
for (i = 0; i < lines.length; i++) {
wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth));
}
this.isWrapping = false;
return wrapped;
},
/**
* Helper function to measure a string of text, given its lineIndex and charIndex offset
* it gets called when charBounds are not available yet.
* @param {CanvasRenderingContext2D} ctx
* @param {String} text
* @param {number} lineIndex
* @param {number} charOffset
* @returns {number}
* @private
*/
_measureWord: function(word, lineIndex, charOffset) {
var width = 0, prevGrapheme, skipLeft = true;
charOffset = charOffset || 0;
for (var i = 0, len = word.length; i < len; i++) {
var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft);
width += box.kernedWidth;
prevGrapheme = word[i];
}
return width;
},
/**
* Wraps a line of text using the width of the Textbox and a context.
* @param {Array} line The grapheme array that represent the line
* @param {Number} lineIndex
* @param {Number} desiredWidth width you want to wrap the line to
* @param {Number} reservedSpace space to remove from wrapping for custom functionalities
* @returns {Array} Array of line(s) into which the given text is wrapped
* to.
*/
_wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) {
var lineWidth = 0,
splitByGrapheme = this.splitByGrapheme,
graphemeLines = [],
line = [],
// spaces in different languges?
words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners),
word = '',
offset = 0,
infix = splitByGrapheme ? '' : ' ',
wordWidth = 0,
infixWidth = 0,
largestWordWidth = 0,
lineJustStarted = true,
additionalSpace = splitByGrapheme ? 0 : this._getWidthOfCharSpacing(),
reservedSpace = reservedSpace || 0;
// fix a difference between split and graphemeSplit
if (words.length === 0) {
words.push([]);
}
desiredWidth -= reservedSpace;
for (var i = 0; i < words.length; i++) {
// if using splitByGrapheme words are already in graphemes.
word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]);
wordWidth = this._measureWord(word, lineIndex, offset);
offset += word.length;
lineWidth += infixWidth + wordWidth - additionalSpace;
if (lineWidth >= desiredWidth && !lineJustStarted) {
graphemeLines.push(line);
line = [];
lineWidth = wordWidth;
lineJustStarted = true;
}
else {
lineWidth += additionalSpace;
}
if (!lineJustStarted && !splitByGrapheme) {
line.push(infix);
}
line = line.concat(word);
infixWidth = this._measureWord([infix], lineIndex, offset);
offset++;
lineJustStarted = false;
// keep track of largest word
if (wordWidth > largestWordWidth) {
largestWordWidth = wordWidth;
}
}
i && graphemeLines.push(line);
if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
}
return graphemeLines;
},
/**
* Detect if the text line is ended with an hard break
* text and itext do not have wrapping, return false
* @param {Number} lineIndex text to split
* @return {Boolean}
*/
isEndOfWrapping: function(lineIndex) {
if (!this._styleMap[lineIndex + 1]) {
// is last line, return true;
return true;
}
if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) {
// this is last line before a line break, return true;
return true;
}
return false;
},
/**
* Detect if a line has a linebreak and so we need to account for it when moving
* and counting style.
* @return Number
*/
missingNewlineOffset: function(lineIndex) {
if (this.splitByGrapheme) {
return this.isEndOfWrapping(lineIndex) ? 1 : 0;
}
return 1;
},
/**
* Gets lines of text to render in the Textbox. This function calculates
* text wrapping on the fly every time it is called.
* @param {String} text text to split
* @returns {Array} Array of lines in the Textbox.
* @override
*/
_splitTextIntoLines: function(text) {
var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text),
graphemeLines = this._wrapText(newText.lines, this.width),
lines = new Array(graphemeLines.length);
for (var i = 0; i < graphemeLines.length; i++) {
lines[i] = graphemeLines[i].join('');
}
newText.lines = lines;
newText.graphemeLines = graphemeLines;
return newText;
},
getMinWidth: function() {
return Math.max(this.minWidth, this.dynamicMinWidth);
},
_removeExtraneousStyles: function() {
var linesToKeep = {};
for (var prop in this._styleMap) {
if (this._textLines[prop]) {
linesToKeep[this._styleMap[prop].line] = 1;
}
}
for (var prop in this.styles) {
if (!linesToKeep[prop]) {
delete this.styles[prop];
}
}
},
/**
* Returns object representation of an instance
* @method toObject
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude));
}
});
/**
* Returns fabric.Textbox instance from an object representation
* @static
* @memberOf fabric.Textbox
* @param {Object} object Object to create an instance from
* @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created
*/
fabric.Textbox.fromObject = function(object, callback) {
return fabric.Object._fromObject('Textbox', object, callback, 'text');
};
})(typeof exports !== 'undefined' ? exports : this);
(function() {
/**
* Override _setObjectScale and add Textbox specific resizing behavior. Resizing
* a Textbox doesn't scale text, it only changes width and makes text wrap automatically.
*/
var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale;
fabric.Canvas.prototype._setObjectScale = function(localMouse, transform,
lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {
var t = transform.target, scaled,
scaleX = localMouse.x * t.scaleX / _dim.x,
scaleY = localMouse.y * t.scaleY / _dim.y;
if (by === 'x' && t instanceof fabric.Textbox) {
var tw = t._getTransformedDimensions().x;
var w = t.width * (localMouse.x / tw);
transform.newScaleX = scaleX;
transform.newScaleY = scaleY;
if (w >= t.getMinWidth()) {
scaled = w !== t.width;
t.set('width', w);
return scaled;
}
}
else {
return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform,
lockScalingX, lockScalingY, by, lockScalingFlip, _dim);
}
};
fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.IText.prototype */ {
/**
* @private
*/
_removeExtraneousStyles: function() {
for (var prop in this._styleMap) {
if (!this._textLines[prop]) {
delete this.styles[this._styleMap[prop].line];
}
}
},
});
})();