mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-14 03:10:59 +00:00
1121 lines
33 KiB
JavaScript
1121 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 Nubmer
|
|
* @default
|
|
*/
|
|
selectionStart: 0,
|
|
|
|
/**
|
|
* Index where text selection ends
|
|
* @type Nubmer
|
|
* @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: { },
|
|
|
|
/**
|
|
* 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();
|
|
|
|
fabric.IText.instances.push(this);
|
|
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_clearCache: function() {
|
|
this.callSuper('_clearCache');
|
|
this.__maxFontHeights = [ ];
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
if (this.styles[loc.lineIndex]) {
|
|
return this.styles[loc.lineIndex][loc.charIndex] || { };
|
|
}
|
|
|
|
return { };
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_extendStyles: function(index, styles) {
|
|
var loc = this.get2DCursorLocation(index);
|
|
|
|
if (!this.styles[loc.lineIndex]) {
|
|
this.styles[loc.lineIndex] = { };
|
|
}
|
|
if (!this.styles[loc.lineIndex][loc.charIndex]) {
|
|
this.styles[loc.lineIndex][loc.charIndex] = { };
|
|
}
|
|
|
|
fabric.util.object.extend(this.styles[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;
|
|
|
|
if (this.selectionStart === this.selectionEnd) {
|
|
boundaries = this._getCursorBoundaries(chars, 'cursor');
|
|
this.renderCursor(boundaries);
|
|
}
|
|
else {
|
|
boundaries = this._getCursorBoundaries(chars, 'selection');
|
|
this.renderSelection(chars, boundaries);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 textBeforeCursor = this.text.slice(0, selectionStart),
|
|
linesBeforeCursor = textBeforeCursor.split(this._reNewline);
|
|
|
|
return {
|
|
lineIndex: linesBeforeCursor.length - 1,
|
|
charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 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.styles[lineIndex] && this.styles[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) {
|
|
return (
|
|
this.styles[lineIndex] &&
|
|
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] &&
|
|
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].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) {
|
|
return (
|
|
this.styles[lineIndex] &&
|
|
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] &&
|
|
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].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._getCachedLineOffset(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
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getCachedLineOffset: function(lineIndex) {
|
|
var widthOfLine = this._getLineWidth(this.ctx, lineIndex);
|
|
|
|
return this.__lineOffsets[lineIndex] ||
|
|
(this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine));
|
|
},
|
|
|
|
/**
|
|
* Renders cursor
|
|
* @param {Object} boundaries
|
|
*/
|
|
renderCursor: function(boundaries) {
|
|
var ctx = this.ctx;
|
|
|
|
ctx.save();
|
|
|
|
var cursorLocation = this.get2DCursorLocation(),
|
|
lineIndex = cursorLocation.lineIndex,
|
|
charIndex = cursorLocation.charIndex,
|
|
charHeight = this.getCurrentCharFontSize(lineIndex, charIndex),
|
|
leftOffset = (lineIndex === 0 && charIndex === 0)
|
|
? this._getCachedLineOffset(lineIndex, this.text.split(this._reNewline))
|
|
: 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);
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Renders text selection
|
|
* @param {Array} chars Array of characters
|
|
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
|
|
*/
|
|
renderSelection: function(chars, boundaries) {
|
|
var ctx = this.ctx;
|
|
|
|
ctx.save();
|
|
|
|
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._getCachedLineOffset(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;
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {String} method
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderChars: function(method, ctx, line, left, top, lineIndex) {
|
|
|
|
if (this.isEmptyStyles()) {
|
|
return this._renderCharsFast(method, ctx, line, left, top);
|
|
}
|
|
|
|
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._getCachedLineOffset(lineIndex),
|
|
chars = line.split(''),
|
|
prevStyle,
|
|
charsToRender = '';
|
|
|
|
left += lineLeftOffset || 0;
|
|
|
|
ctx.save();
|
|
top -= lineHeight / this.lineHeight * this._fontSizeFraction;
|
|
for (var i = 0, len = chars.length; i <= len; i++) {
|
|
prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i);
|
|
var 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 += chars[i];
|
|
}
|
|
|
|
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 decl, charWidth, charHeight,
|
|
offset = this._fontSizeFraction * lineHeight / this.lineHeight;
|
|
|
|
if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) {
|
|
|
|
var shouldStroke = decl.stroke || this.stroke,
|
|
shouldFill = decl.fill || this.fill;
|
|
|
|
ctx.save();
|
|
charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl);
|
|
charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i);
|
|
|
|
if (shouldFill) {
|
|
ctx.fillText(_char, left, top);
|
|
}
|
|
if (shouldStroke) {
|
|
ctx.strokeText(_char, left, top);
|
|
}
|
|
|
|
this._renderCharDecoration(ctx, decl, left, top, offset, charWidth, charHeight);
|
|
ctx.restore();
|
|
|
|
ctx.translate(charWidth, 0);
|
|
}
|
|
else {
|
|
if (method === 'strokeText' && this.stroke) {
|
|
ctx[method](_char, left, top);
|
|
}
|
|
if (method === 'fillText' && this.fill) {
|
|
ctx[method](_char, left, top);
|
|
}
|
|
charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i);
|
|
this._renderCharDecoration(ctx, null, left, top, offset, charWidth, this.fontSize);
|
|
|
|
ctx.translate(ctx.measureText(_char).width, 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, styleDeclaration, left, top, offset, charWidth, charHeight) {
|
|
|
|
var textDecoration = styleDeclaration
|
|
? (styleDeclaration.textDecoration || this.textDecoration)
|
|
: this.textDecoration;
|
|
|
|
if (!textDecoration) {
|
|
return;
|
|
}
|
|
|
|
if (textDecoration.indexOf('underline') > -1) {
|
|
ctx.fillRect(
|
|
left,
|
|
top + charHeight / 10,
|
|
charWidth ,
|
|
charHeight / 15
|
|
);
|
|
}
|
|
if (textDecoration.indexOf('line-through') > -1) {
|
|
ctx.fillRect(
|
|
left,
|
|
top - charHeight * (this._fontSizeFraction + this._fontSizeMult - 1) + charHeight / 15,
|
|
charWidth,
|
|
charHeight / 15
|
|
);
|
|
}
|
|
if (textDecoration.indexOf('overline') > -1) {
|
|
ctx.fillRect(
|
|
left,
|
|
top - (this._fontSizeMult - this._fontSizeFraction) * charHeight,
|
|
charWidth,
|
|
charHeight / 15
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {String} method
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {String} line
|
|
*/
|
|
_renderTextLine: function(method, ctx, line, left, top, lineIndex) {
|
|
// to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine
|
|
// the adding 0.03 is just to align text with itext by overlap test
|
|
if (!this.isEmptyStyles()) {
|
|
top += this.fontSize * (this._fontSizeFraction + 0.03);
|
|
}
|
|
this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextDecoration: function(ctx) {
|
|
if (this.isEmptyStyles()) {
|
|
return this.callSuper('_renderTextDecoration', ctx);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextLinesBackground: function(ctx) {
|
|
if (!this.textBackgroundColor && !this.styles) {
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
|
|
if (this.textBackgroundColor) {
|
|
ctx.fillStyle = this.textBackgroundColor;
|
|
}
|
|
|
|
var lineHeights = 0;
|
|
|
|
for (var i = 0, len = this._textLines.length; i < len; i++) {
|
|
|
|
var heightOfLine = this._getHeightOfLine(ctx, i);
|
|
if (this._textLines[i] === '') {
|
|
lineHeights += heightOfLine;
|
|
continue;
|
|
}
|
|
|
|
var lineWidth = this._getLineWidth(ctx, i),
|
|
lineLeftOffset = this._getCachedLineOffset(i);
|
|
|
|
if (this.textBackgroundColor) {
|
|
ctx.fillStyle = this.textBackgroundColor;
|
|
|
|
ctx.fillRect(
|
|
this._getLeftOffset() + lineLeftOffset,
|
|
this._getTopOffset() + lineHeights,
|
|
lineWidth,
|
|
heightOfLine / this.lineHeight
|
|
);
|
|
}
|
|
if (this.styles[i]) {
|
|
for (var j = 0, jlen = this._textLines[i].length; j < jlen; j++) {
|
|
if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) {
|
|
|
|
var _char = this._textLines[i][j];
|
|
|
|
ctx.fillStyle = this.styles[i][j].textBackgroundColor;
|
|
|
|
ctx.fillRect(
|
|
this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j),
|
|
this._getTopOffset() + lineHeights,
|
|
this._getWidthOfChar(ctx, _char, i, j) + 1,
|
|
heightOfLine / this.lineHeight
|
|
);
|
|
}
|
|
}
|
|
}
|
|
lineHeights += heightOfLine;
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getCacheProp: function(_char, styleDeclaration) {
|
|
return _char +
|
|
styleDeclaration.fontFamily +
|
|
styleDeclaration.fontSize +
|
|
styleDeclaration.fontWeight +
|
|
styleDeclaration.fontStyle +
|
|
styleDeclaration.shadow;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {String} _char
|
|
* @param {Number} lineIndex
|
|
* @param {Number} charIndex
|
|
* @param {Object} [decl]
|
|
*/
|
|
_applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) {
|
|
var styleDeclaration = decl ||
|
|
(this.styles[lineIndex] &&
|
|
this.styles[lineIndex][charIndex]);
|
|
|
|
if (styleDeclaration) {
|
|
// cloning so that original style object is not polluted with following font declarations
|
|
styleDeclaration = clone(styleDeclaration);
|
|
}
|
|
else {
|
|
styleDeclaration = { };
|
|
}
|
|
|
|
this._applyFontStyles(styleDeclaration);
|
|
|
|
var cacheProp = this._getCacheProp(_char, styleDeclaration);
|
|
|
|
// short-circuit if no styles
|
|
if (this.isEmptyStyles() && 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)
|
|
: fill;
|
|
|
|
if (styleDeclaration.stroke) {
|
|
ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive)
|
|
? styleDeclaration.stroke.toLive(ctx)
|
|
: styleDeclaration.stroke;
|
|
}
|
|
|
|
ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth;
|
|
ctx.font = this._getFontDeclaration.call(styleDeclaration);
|
|
this._setShadow.call(styleDeclaration, ctx);
|
|
|
|
if (!this.caching) {
|
|
return ctx.measureText(_char).width;
|
|
}
|
|
|
|
if (!this._charWidthsCache[cacheProp]) {
|
|
this._charWidthsCache[cacheProp] = ctx.measureText(_char).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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Number} lineIndex
|
|
* @param {Number} charIndex
|
|
*/
|
|
_getStyleDeclaration: function(lineIndex, charIndex) {
|
|
return (this.styles[lineIndex] && this.styles[lineIndex][charIndex])
|
|
? clone(this.styles[lineIndex][charIndex])
|
|
: { };
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_getWidthOfChar: function(ctx, _char, lineIndex, charIndex) {
|
|
if (this.textAlign === 'justify' && /\s/.test(_char)) {
|
|
return this._getWidthOfSpace(ctx, lineIndex);
|
|
}
|
|
|
|
var styleDeclaration = this._getStyleDeclaration(lineIndex, charIndex);
|
|
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, _char, lineIndex, charIndex) {
|
|
if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) {
|
|
return this.styles[lineIndex][charIndex].fontSize || this.fontSize;
|
|
}
|
|
return this.fontSize;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_getWidthOfCharAt: function(ctx, lineIndex, charIndex) {
|
|
var _char = this._textLines[lineIndex].split('')[charIndex];
|
|
return this._getWidthOfChar(ctx, _char, lineIndex, charIndex);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_getHeightOfCharAt: function(ctx, lineIndex, charIndex) {
|
|
var _char = this._textLines[lineIndex].split('')[charIndex];
|
|
return this._getHeightOfChar(ctx, _char, lineIndex, charIndex);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_getWidthOfCharsAt: function(ctx, lineIndex, charIndex) {
|
|
var width = 0;
|
|
for (var i = 0; i < charIndex; i++) {
|
|
width += this._getWidthOfCharAt(ctx, 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) {
|
|
var lines = this.text.split(this._reNewline),
|
|
line = lines[lineIndex],
|
|
words = line.split(/\s+/),
|
|
wordsWidth = this._getWidthOfWords(ctx, line, lineIndex),
|
|
widthDiff = this.width - wordsWidth,
|
|
numSpaces = words.length - 1,
|
|
width = widthDiff / numSpaces;
|
|
|
|
return width;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Number} line
|
|
* @param {Number} lineIndex
|
|
*/
|
|
_getWidthOfWords: function (ctx, line, lineIndex) {
|
|
var width = 0;
|
|
|
|
for (var charIndex = 0; charIndex < line.length; charIndex++) {
|
|
var _char = line[charIndex];
|
|
|
|
if (!_char.match(/\s/)) {
|
|
width += this._getWidthOfChar(ctx, _char, lineIndex, charIndex);
|
|
}
|
|
}
|
|
|
|
return width;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_getHeightOfLine: function(ctx, lineIndex) {
|
|
if (this.__lineHeights[lineIndex]) {
|
|
return this.__lineHeights[lineIndex];
|
|
}
|
|
|
|
var line = this._textLines[lineIndex],
|
|
maxHeight = this._getHeightOfChar(ctx, line[0], lineIndex, 0),
|
|
chars = line.split('');
|
|
|
|
for (var i = 1, len = chars.length; i < len; i++) {
|
|
var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i);
|
|
if (currentCharHeight > maxHeight) {
|
|
maxHeight = currentCharHeight;
|
|
}
|
|
}
|
|
this.__maxFontHeights[lineIndex] = maxHeight;
|
|
this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;
|
|
return this.__lineHeights[lineIndex];
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_getTextHeight: function(ctx) {
|
|
var height = 0;
|
|
for (var i = 0, len = this._textLines.length; i < len; i++) {
|
|
height += this._getHeightOfLine(ctx, i);
|
|
}
|
|
return height;
|
|
},
|
|
|
|
/**
|
|
* This method is overwritten to account for different top offset
|
|
* @private
|
|
*/
|
|
_renderTextBoxBackground: function(ctx) {
|
|
if (!this.backgroundColor) {
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = this.backgroundColor;
|
|
|
|
ctx.fillRect(
|
|
this._getLeftOffset(),
|
|
this._getTopOffset(),
|
|
this.width,
|
|
this.height
|
|
);
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @method toObject
|
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toObject: function(propertiesToInclude) {
|
|
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
|
|
styles: clone(this.styles)
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returns fabric.IText instance from an object representation
|
|
* @static
|
|
* @memberOf fabric.IText
|
|
* @param {Object} object Object to create an instance from
|
|
* @return {fabric.IText} instance of fabric.IText
|
|
*/
|
|
fabric.IText.fromObject = function(object) {
|
|
return new fabric.IText(object.text, clone(object));
|
|
};
|
|
|
|
/**
|
|
* Contains all fabric.IText objects that have been created
|
|
* @static
|
|
* @memberof fabric.IText
|
|
* @type Array
|
|
*/
|
|
fabric.IText.instances = [ ];
|
|
|
|
})();
|