fabric.js/src/shapes/itext.class.js
2016-02-25 02:59:04 +01:00

1090 lines
33 KiB
JavaScript

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