';
},
/**
* Return the dimension and the zoom level needed to create a cache canvas
* big enough to host the object to be cached.
* @private
* @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 dim = this.callSuper('_getCacheCanvasDimensions');
var fontSize = this.fontSize * 2;
dim.width += fontSize * dim.zoomX;
dim.height += fontSize * dim.zoomY;
return dim;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
this._setTextStyles(ctx);
if (this.group && this.group.type === 'path-group') {
ctx.translate(this.left, this.top);
}
this._renderTextLinesBackground(ctx);
this._renderText(ctx);
this._renderTextDecoration(ctx);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderText: function(ctx) {
this._renderTextFill(ctx);
this._renderTextStroke(ctx);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_setTextStyles: function(ctx) {
ctx.textBaseline = 'alphabetic';
ctx.font = this._getFontDeclaration();
},
/**
* @private
* @return {Number} Height of fabric.Text object
*/
_getTextHeight: function() {
return this._getHeightOfSingleLine() + (this._textLines.length - 1) * this._getHeightOfLine();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @return {Number} Maximum width of fabric.Text object
*/
_getTextWidth: function(ctx) {
var maxWidth = this._getLineWidth(ctx, 0);
for (var i = 1, len = this._textLines.length; i < len; i++) {
var currentLineWidth = this._getLineWidth(ctx, 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} chars Chars to render
* @param {Number} left Left position of text
* @param {Number} top Top position of text
*/
_renderChars: function(method, ctx, chars, left, top) {
// remove Text word from method var
var shortM = method.slice(0, -4), char, width;
if (this[shortM].toLive) {
var offsetX = -this.width / 2 + this[shortM].offsetX || 0,
offsetY = -this.height / 2 + this[shortM].offsetY || 0;
ctx.save();
ctx.translate(offsetX, offsetY);
left -= offsetX;
top -= offsetY;
}
if (this.charSpacing !== 0) {
var additionalSpace = this._getWidthOfCharSpacing();
chars = chars.split('');
for (var i = 0, len = chars.length; i < len; i++) {
char = chars[i];
width = ctx.measureText(char).width + additionalSpace;
ctx[method](char, left, top);
left += width > 0 ? width : 0;
}
}
else {
ctx[method](chars, left, top);
}
this[shortM].toLive && ctx.restore();
},
/**
* @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) {
// lift the line by quarter of fontSize
top -= this.fontSize * this._fontSizeFraction;
// short-circuit
var lineWidth = this._getLineWidth(ctx, lineIndex);
if (this.textAlign !== 'justify' || this.width < lineWidth) {
this._renderChars(method, ctx, line, left, top, lineIndex);
return;
}
// stretch the line
var words = line.split(/\s+/),
charOffset = 0,
wordsWidth = this._getWidthOfWords(ctx, words.join(' '), lineIndex, 0),
widthDiff = this.width - wordsWidth,
numSpaces = words.length - 1,
spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0,
leftOffset = 0, word;
for (var i = 0, len = words.length; i < len; i++) {
while (line[charOffset] === ' ' && charOffset < line.length) {
charOffset++;
}
word = words[i];
this._renderChars(method, ctx, word, left + leftOffset, top, lineIndex, charOffset);
leftOffset += this._getWidthOfWords(ctx, word, lineIndex, charOffset) + spaceWidth;
charOffset += word.length;
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} word
*/
_getWidthOfWords: function (ctx, word) {
var width = ctx.measureText(word).width, charCount, additionalSpace;
if (this.charSpacing !== 0) {
charCount = word.split('').length;
additionalSpace = charCount * this._getWidthOfCharSpacing();
width += additionalSpace;
}
return width > 0 ? width : 0;
},
/**
* @private
* @return {Number} Left offset
*/
_getLeftOffset: function() {
return -this.width / 2;
},
/**
* @private
* @return {Number} Top offset
*/
_getTopOffset: function() {
return -this.height / 2;
},
/**
* Returns true because text has no style
*/
isEmptyStyles: function() {
return true;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} method Method name ("fillText" or "strokeText")
*/
_renderTextCommon: function(ctx, method) {
var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset();
for (var i = 0, len = this._textLines.length; i < len; i++) {
var heightOfLine = this._getHeightOfLine(ctx, i),
maxHeight = heightOfLine / this.lineHeight,
lineWidth = this._getLineWidth(ctx, i),
leftOffset = this._getLineLeftOffset(lineWidth);
this._renderTextLine(
method,
ctx,
this._textLines[i],
left + leftOffset,
top + lineHeights + maxHeight,
i
);
lineHeights += heightOfLine;
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextFill: function(ctx) {
if (!this.fill && this.isEmptyStyles()) {
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
* @return {Number} height of line
*/
_getHeightOfLine: function() {
return this._getHeightOfSingleLine() * this.lineHeight;
},
/**
* @private
* @return {Number} height of line without lineHeight
*/
_getHeightOfSingleLine: function() {
return this.fontSize * this._fontSizeMult;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextLinesBackground: function(ctx) {
if (!this.textBackgroundColor) {
return;
}
var lineTopOffset = 0, heightOfLine,
lineWidth, lineLeftOffset, originalFill = ctx.fillStyle;
ctx.fillStyle = this.textBackgroundColor;
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this._getHeightOfLine(ctx, i);
lineWidth = this._getLineWidth(ctx, i);
if (lineWidth > 0) {
lineLeftOffset = this._getLineLeftOffset(lineWidth);
ctx.fillRect(
this._getLeftOffset() + lineLeftOffset,
this._getTopOffset() + lineTopOffset,
lineWidth,
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 {Number} lineWidth Width of text line
* @return {Number} Line left offset
*/
_getLineLeftOffset: function(lineWidth) {
if (this.textAlign === 'center') {
return (this.width - lineWidth) / 2;
}
if (this.textAlign === 'right') {
return this.width - lineWidth;
}
return 0;
},
/**
* @private
*/
_clearCache: function() {
this.__lineWidths = [];
this.__lineHeights = [];
},
/**
* @private
*/
_shouldClearDimensionCache: function() {
var shouldClear = this._forceClearCache;
shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps'));
if (shouldClear) {
this.saveState({ propertySet: '_dimensionAffectingProps' });
this.dirty = true;
}
return shouldClear;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
_getLineWidth: function(ctx, lineIndex) {
if (this.__lineWidths[lineIndex]) {
return this.__lineWidths[lineIndex] === -1 ? this.width : this.__lineWidths[lineIndex];
}
var width, wordCount, line = this._textLines[lineIndex];
if (line === '') {
width = 0;
}
else {
width = this._measureLine(ctx, lineIndex);
}
this.__lineWidths[lineIndex] = width;
if (width && this.textAlign === 'justify') {
wordCount = line.split(/\s+/);
if (wordCount.length > 1) {
this.__lineWidths[lineIndex] = -1;
}
}
return width;
},
_getWidthOfCharSpacing: function() {
if (this.charSpacing !== 0) {
return this.fontSize * this.charSpacing / 1000;
}
return 0;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
_measureLine: function(ctx, lineIndex) {
var line = this._textLines[lineIndex],
width = ctx.measureText(line).width,
additionalSpace = 0, charCount, finalWidth;
if (this.charSpacing !== 0) {
charCount = line.split('').length;
additionalSpace = (charCount - 1) * this._getWidthOfCharSpacing();
}
finalWidth = width + additionalSpace;
return finalWidth > 0 ? finalWidth : 0;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextDecoration: function(ctx) {
if (!this.textDecoration) {
return;
}
var halfOfVerticalBox = this.height / 2,
_this = this, offsets = [];
/** @ignore */
function renderLinesAtOffset(offsets) {
var i, lineHeight = 0, len, j, oLen, lineWidth,
lineLeftOffset, heightOfLine;
for (i = 0, len = _this._textLines.length; i < len; i++) {
lineWidth = _this._getLineWidth(ctx, i);
lineLeftOffset = _this._getLineLeftOffset(lineWidth);
heightOfLine = _this._getHeightOfLine(ctx, i);
for (j = 0, oLen = offsets.length; j < oLen; j++) {
ctx.fillRect(
_this._getLeftOffset() + lineLeftOffset,
lineHeight + (_this._fontSizeMult - 1 + offsets[j] ) * _this.fontSize - halfOfVerticalBox,
lineWidth,
_this.fontSize / 15);
}
lineHeight += heightOfLine;
}
}
if (this.textDecoration.indexOf('underline') > -1) {
offsets.push(0.85); // 1 - 3/16
}
if (this.textDecoration.indexOf('line-through') > -1) {
offsets.push(0.43);
}
if (this.textDecoration.indexOf('overline') > -1) {
offsets.push(-0.12);
}
if (offsets.length > 0) {
renderLinesAtOffset(offsets);
}
},
/**
* return font declaration string for canvas context
* @returns {String} font declaration formatted for canvas context.
*/
_getFontDeclaration: function() {
return [
// node-canvas needs "weight style", while browsers need "style weight"
(fabric.isLikelyNode ? this.fontWeight : this.fontStyle),
(fabric.isLikelyNode ? this.fontStyle : this.fontWeight),
this.fontSize + 'px',
(fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily)
].join(' ');
},
/**
* Renders text instance on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} noTransform
*/
render: function(ctx, noTransform) {
// 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._setTextStyles(ctx);
this._initDimensions(ctx);
}
this.callSuper('render', ctx, noTransform);
},
/**
* Returns the text as an array of lines.
* @returns {Array} Lines in the text
*/
_splitTextIntoLines: function() {
return this.text.split(this._reNewline);
},
/**
* 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',
'textDecoration',
'textAlign',
'textBackgroundColor',
'charSpacing'
].concat(propertiesToInclude);
return this.callSuper('toObject', additionalProperties);
},
/* _TO_SVG_START_ */
/**
* 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) {
if (!this.ctx) {
this.ctx = fabric.util.createCanvasElement().getContext('2d');
}
var markup = this._createBaseSVGMarkup(),
offsets = this._getSVGLeftTopOffsets(this.ctx),
textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
this._wrapSVGTextAndBg(markup, textAndBg);
return reviver ? reviver(markup.join('')) : markup.join('');
},
/**
* @private
*/
_getSVGLeftTopOffsets: function(ctx) {
var lineTop = this._getHeightOfLine(ctx, 0),
textLeft = -this.width / 2,
textTop = 0;
return {
textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0),
textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0),
lineTop: lineTop
};
},
/**
* @private
*/
_wrapSVGTextAndBg: function(markup, textAndBg) {
var noShadow = true, filter = this.getSvgFilter(),
style = filter === '' ? '' : ' style="' + filter + '"';
markup.push(
'\t\n',
textAndBg.textBgRects.join(''),
'\t\t\n',
textAndBg.textSpans.join(''),
'\t\t\n',
'\t\n'
);
},
/**
* @private
* @param {Number} textTopOffset Text top offset
* @param {Number} textLeftOffset Text left offset
* @return {Object}
*/
_getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
var textSpans = [],
textBgRects = [],
height = 0;
// bounding-box background
this._setSVGBg(textBgRects);
// text and text-background
for (var i = 0, len = this._textLines.length; i < len; i++) {
if (this.textBackgroundColor) {
this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height);
}
this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects);
height += this._getHeightOfLine(this.ctx, i);
}
return {
textSpans: textSpans,
textBgRects: textBgRects
};
},
_setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) {
var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction)
- textTopOffset + height - this.height / 2;
if (this.textAlign === 'justify') {
// i call from here to do not intefere with IText
this._setSVGTextLineJustifed(i, textSpans, yPos, textLeftOffset);
return;
}
textSpans.push(
'\t\t\t elements since setting opacity
// on containing one doesn't work in Illustrator
this._getFillAttributes(this.fill), '>',
fabric.util.string.escapeXml(this._textLines[i]),
'\n'
);
},
_setSVGTextLineJustifed: function(i, textSpans, yPos, textLeftOffset) {
var ctx = fabric.util.createCanvasElement().getContext('2d');
this._setTextStyles(ctx);
var line = this._textLines[i],
words = line.split(/\s+/),
wordsWidth = this._getWidthOfWords(ctx, words.join('')),
widthDiff = this.width - wordsWidth,
numSpaces = words.length - 1,
spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0,
word, attributes = this._getFillAttributes(this.fill),
len;
textLeftOffset += this._getLineLeftOffset(this._getLineWidth(ctx, i));
for (i = 0, len = words.length; i < len; i++) {
word = words[i];
textSpans.push(
'\t\t\t elements since setting opacity
// on containing one doesn't work in Illustrator
attributes, '>',
fabric.util.string.escapeXml(word),
'\n'
);
textLeftOffset += this._getWidthOfWords(ctx, word) + spaceWidth;
}
},
_setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) {
textBgRects.push(
'\t\t\n');
},
_setSVGBg: function(textBgRects) {
if (this.backgroundColor) {
textBgRects.push(
'\t\t\n');
}
},
/**
* 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() + '"';
},
/* _TO_SVG_END_ */
/**
* Sets specified property to a specified value
* @param {String} key
* @param {*} value
* @return {fabric.Text} thisArg
* @chainable
*/
_set: function(key, value) {
this.callSuper('_set', key, value);
if (this._dimensionAffectingProps.indexOf(key) > -1) {
this._initDimensions();
this.setCoords();
}
},
/**
* 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 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 {Object} [options] Options object
* @return {fabric.Text} Instance of fabric.Text
*/
fabric.Text.fromElement = function(element, options) {
if (!element) {
return null;
}
var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES);
options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes);
options.top = options.top || 0;
options.left = options.left || 0;
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;
}
if (!options.originX) {
options.originX = 'left';
}
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 text = new fabric.Text(textContent, options),
textHeightScaleFactor = text.getHeight() / text.height,
lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height,
scaledDiff = lineHeightDiff * textHeightScaleFactor,
textHeight = text.getHeight() + scaledDiff,
offX = 0;
/*
Adjust positioning:
x/y attributes in SVG correspond to the bottom-left corner of text bounding box
top/left properties in Fabric correspond to center point of text bounding box
*/
if (text.originX === 'left') {
offX = text.getWidth() / 2;
}
if (text.originX === 'right') {
offX = -text.getWidth() / 2;
}
text.set({
left: text.getLeft() + offX,
top: text.getTop() - textHeight / 2 + text.fontSize * (0.18 + text._fontSizeFraction) / text.lineHeight /* 0.3 is the old lineHeight */
});
return 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
* @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first
* @return {fabric.Text} Instance of fabric.Text
*/
fabric.Text.fromObject = function(object, callback, forceAsync) {
return fabric.Object._fromObject('Text', object, callback, forceAsync, 'text');
};
fabric.util.createAccessors(fabric.Text);
})(typeof exports !== 'undefined' ? exports : this);
(function() {
var clone = fabric.util.object.clone;
/**
* 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,
/**
* 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
*/
_reSpace: /\s|\n/,
/**
* @private
*/
_currentCursorOpacity: 0,
/**
* @private
*/
_selectionDirection: null,
/**
* @private
*/
_abortCursorAnimation: false,
/**
* @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]) {
// eslint-disable-next-line no-unused-vars
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);
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 });
},
/**
* 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);
},
/**
* 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.
* @param {CanvasRenderingContext2D} [ctx] Context to render on
* @private
*/
_initDimensions: function(ctx) {
if (!ctx) {
this.clearContextTop();
}
this.callSuper('_initDimensions', ctx);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} noTransform
*/
render: function(ctx, noTransform) {
this.clearContextTop();
this.callSuper('render', ctx, noTransform);
// 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);
this.ctx = ctx;
},
/**
* Prepare and clean the contextTop
*/
clearContextTop: function() {
if (!this.active || !this.isEditing) {
return;
}
if (this.canvas && this.canvas.contextTop) {
var ctx = this.canvas.contextTop;
ctx.save();
ctx.transform.apply(ctx, this.canvas.viewportTransform);
this.transform(ctx);
this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);
this._clearTextArea(ctx);
ctx.restore();
}
},
/**
* Renders cursor or selection (depending on what exists)
*/
renderCursorOrSelection: function() {
if (!this.active || !this.isEditing) {
return;
}
var chars = this.text.split(''),
boundaries, ctx;
if (this.canvas && 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);
this._clearTextArea(ctx);
}
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();
},
_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 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) {
if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
return this.cursorOffsetCache;
}
var lineLeftOffset = 0,
lineIndex = 0,
charIndex = 0,
topOffset = 0,
leftOffset = 0,
boundaries;
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);
}
if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
leftOffset -= this._getWidthOfCharSpacing();
}
boundaries = {
top: topOffset,
left: leftOffset > 0 ? leftOffset : 0,
lineLeft: lineLeftOffset
};
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,
charHeight = this.getCurrentCharFontSize(lineIndex, charIndex),
leftOffset = boundaries.leftOffset,
multiplier = this.scaleX * this.canvas.getZoom(),
cursorWidth = this.cursorWidth / multiplier;
ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex);
ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
ctx.fillRect(
boundaries.left + leftOffset - cursorWidth / 2,
boundaries.top + boundaries.topOffset,
cursorWidth,
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),
realLineHeight = 0, 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);
}
}
if (j === line.length) {
boxWidth -= this._getWidthOfCharSpacing();
}
}
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);
}
if (end.charIndex === line.length) {
boxWidth -= this._getWidthOfCharSpacing();
}
}
realLineHeight = lineHeight;
if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {
lineHeight /= this.lineHeight;
}
ctx.fillRect(
boundaries.left + lineOffset,
boundaries.top + boundaries.topOffset,
boxWidth > 0 ? boxWidth : 0,
lineHeight);
boundaries.topOffset += realLineHeight;
}
},
/**
* @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, charOffset) {
if (this.isEmptyStyles()) {
return this._renderCharsFast(method, ctx, line, left, top);
}
charOffset = charOffset || 0;
// set proper line offset
var lineHeight = this._getHeightOfLine(ctx, lineIndex),
prevStyle,
thisStyle,
charsToRender = '';
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) {
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, chars, additionalSpace, _charWidth;
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 || null);
textDecoration = textDecoration || this.textDecoration;
if (decl && decl.textBackgroundColor) {
this._removeShadow(ctx);
}
if (this.charSpacing !== 0) {
additionalSpace = this._getWidthOfCharSpacing();
chars = _char.split('');
charWidth = 0;
for (var j = 0, len = chars.length, char; j < len; j++) {
char = chars[j];
shouldFill && ctx.fillText(char, left + charWidth, top);
shouldStroke && ctx.strokeText(char, left + charWidth, top);
_charWidth = ctx.measureText(char).width + additionalSpace;
charWidth += _charWidth > 0 ? _charWidth : 0;
}
}
else {
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
* @param {Number} left
* @param {Number} top
* @param {Number} lineIndex
*/
_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(),
colorCache = '',
line, _char, style, leftCache,
topCache, widthCache, heightCache;
ctx.save();
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);
leftCache = topCache = widthCache = heightCache = 0;
for (var j = 0, jlen = line.length; j < jlen; j++) {
style = this._getStyleDeclaration(i, j) || {};
if (colorCache !== style.textBackgroundColor) {
if (heightCache && widthCache) {
ctx.fillStyle = colorCache;
ctx.fillRect(leftCache, topCache, widthCache, heightCache);
}
leftCache = topCache = widthCache = heightCache = 0;
colorCache = style.textBackgroundColor || '';
}
if (!style.textBackgroundColor) {
colorCache = '';
continue;
}
_char = line[j];
if (colorCache === style.textBackgroundColor) {
colorCache = style.textBackgroundColor;
if (!leftCache) {
leftCache = leftOffset + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j);
}
topCache = topOffset + lineTopOffset;
widthCache += this._getWidthOfChar(ctx, _char, i, j);
heightCache = heightOfLine / this.lineHeight;
}
}
// if a textBackgroundColor ends on the last character of a line
if (heightCache && widthCache) {
ctx.fillStyle = colorCache;
ctx.fillRect(leftCache, topCache, widthCache, heightCache);
leftCache = topCache = widthCache = heightCache = 0;
}
lineTopOffset += heightOfLine;
}
ctx.restore();
},
/**
* @private
*/
_getCacheProp: function(_char, styleDeclaration) {
return _char +
styleDeclaration.fontSize +
styleDeclaration.fontWeight +
styleDeclaration.fontStyle;
},
/**
* @private
* @param {String} fontFamily name
* @return {Object} reference to cache
*/
_getFontCache: function(fontFamily) {
if (!fabric.charWidthsCache[fontFamily]) {
fabric.charWidthsCache[fontFamily] = { };
}
return fabric.charWidthsCache[fontFamily];
},
/**
* @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 = decl || this._getStyleDeclaration(lineIndex, charIndex),
styleDeclaration = clone(charDecl),
width, cacheProp, charWidthsCache;
this._applyFontStyles(styleDeclaration);
charWidthsCache = this._getFontCache(styleDeclaration.fontFamily);
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 && charWidthsCache[cacheProp] && this.caching) {
return 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);
//if we want this._setShadow.call to work with styleDeclarion
//we have to add those references
if (styleDeclaration.shadow) {
styleDeclaration.scaleX = this.scaleX;
styleDeclaration.scaleY = this.scaleY;
styleDeclaration.canvas = this.canvas;
styleDeclaration.getObjectScaling = this.getObjectScaling;
this._setShadow.call(styleDeclaration, ctx);
}
if (!this.caching || !charWidthsCache[cacheProp]) {
width = ctx.measureText(_char).width;
this.caching && (charWidthsCache[cacheProp] = width);
return width;
}
return 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._isMeasuring && this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) {
return this._getWidthOfSpace(ctx, lineIndex);
}
ctx.save();
var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex);
if (this.charSpacing !== 0) {
width += this._getWidthOfCharSpacing();
}
ctx.restore();
return width > 0 ? width : 0;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Number} lineIndex
* @param {Number} charIndex
*/
_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
* @param {Number} lineIndex
* @param {Number} charIndex
*/
_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
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
_measureLine: function(ctx, lineIndex) {
this._isMeasuring = true;
var width = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length);
if (this.charSpacing !== 0) {
width -= this._getWidthOfCharSpacing();
}
this._isMeasuring = false;
return width > 0 ? width : 0;
},
/**
* @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 = Math.max(widthDiff / numSpaces, ctx.measureText(' ').width);
this.__widthOfSpace[lineIndex] = width;
return width;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line
* @param {Number} lineIndex
* @param {Number} charOffset
*/
_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 lineHeight, height = 0;
for (var i = 0, len = this._textLines.length; i < len; i++) {
lineHeight = this._getHeightOfLine(ctx, i);
height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight);
}
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) {
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
styles: clone(this.styles, true)
});
}
});
/**
* 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
* @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first
* @return {fabric.IText} instance of fabric.IText
*/
fabric.IText.fromObject = function(object, callback, forceAsync) {
return fabric.Object._fromObject('IText', object, callback, forceAsync, '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;
});
}
}).bind(this);
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;
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) {
this.canvas && this.canvas.clearContext(this.canvas.contextTop || this.ctx);
}
},
/**
* Selects entire text
*/
selectAll: function() {
this.selectionStart = 0;
this.selectionEnd = this.text.length;
this._fireSelectionChanged();
this._updateTextarea();
},
/**
* Returns selected text
* @return {String}
*/
getSelectedText: function() {
return this.text.slice(this.selectionStart, this.selectionEnd);
},
/**
* Find new selection index representing start of current word according to current selection index
* @param {Number} startFrom Surrent 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.charAt(index))) {
while (this._reSpace.test(this.text.charAt(index))) {
offset++;
index--;
}
}
while (/\S/.test(this.text.charAt(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.charAt(index))) {
while (this._reSpace.test(this.text.charAt(index))) {
offset++;
index++;
}
}
while (/\S/.test(this.text.charAt(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.charAt(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.charAt(index)) && index < this.text.length) {
offset++;
index++;
}
return startFrom + offset;
},
/**
* Returns number of newlines in selected text
* @return {Number} Number of newlines in selected text
*/
getNumNewLinesInSelectedText: function() {
var selectedText = this.getSelectedText(),
numNewLines = 0;
for (var i = 0, len = selectedText.length; i < len; i++) {
if (selectedText[i] === '\n') {
numNewLines++;
}
}
return numNewLines;
},
/**
* 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.charAt(selectionStart)) ? selectionStart - 1 : selectionStart,
_char = this.text.charAt(index),
reNonWord = /[ \n\.,;!\?\-]/;
while (!reNonWord.test(_char) && index > 0 && index < this.text.length) {
index += direction;
_char = this.text.charAt(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
*/
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();
},
/**
* Enters editing state
* @return {fabric.IText} thisArg
* @chainable
*/
enterEditing: function(e) {
if (this.isEditing || !this.editable) {
return;
}
if (this.canvas) {
this.exitEditingOnOthers(this.canvas);
}
this.isEditing = true;
this.selected = true;
this.initHiddenTextarea(e);
this.hiddenTextarea.focus();
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.renderAll();
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;
//console.log(this.borderColor)
this.hasControls = this.selectable = false;
this.lockMovementX = this.lockMovementY = true;
},
/**
* @private
*/
_updateTextarea: function() {
if (!this.hiddenTextarea || this.inCompositionMode) {
return;
}
this.cursorOffsetCache = { };
this.hiddenTextarea.value = this.text;
this.hiddenTextarea.selectionStart = this.selectionStart;
this.hiddenTextarea.selectionEnd = this.selectionEnd;
if (this.selectionStart === this.selectionEnd) {
var style = this._calcTextareaPosition();
this.hiddenTextarea.style.left = style.left;
this.hiddenTextarea.style.top = style.top;
this.hiddenTextarea.style.fontSize = style.fontSize;
}
},
/**
* @private
* @return {Object} style contains style for hiddenTextarea
*/
_calcTextareaPosition: function() {
if (!this.canvas) {
return { x: 1, y: 1 };
}
var chars = this.text.split(''),
boundaries = this._getCursorBoundaries(chars, 'cursor'),
cursorLocation = this.get2DCursorLocation(),
lineIndex = cursorLocation.lineIndex,
charIndex = cursorLocation.charIndex,
charHeight = this.getCurrentCharFontSize(lineIndex, charIndex),
leftOffset = boundaries.leftOffset,
m = this.calcTransformMatrix(),
p = {
x: boundaries.left + leftOffset,
y: boundaries.top + boundaries.topOffset + charHeight
},
upperCanvas = this.canvas.upperCanvasEl,
maxWidth = upperCanvas.width - charHeight,
maxHeight = upperCanvas.height - charHeight;
p = fabric.util.transformPoint(p, m);
p = fabric.util.transformPoint(p, this.canvas.viewportTransform);
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 };
},
/**
* @private
*/
_saveEditingProps: function() {
this._savedProps = {
hasControls: this.hasControls,
borderColor: this.borderColor,
lockMovementX: this.lockMovementX,
lockMovementY: this.lockMovementY,
hoverCursor: this.hoverCursor,
defaultCursor: this.canvas && this.canvas.defaultCursor,
moveCursor: this.canvas && this.canvas.moveCursor
};
},
/**
* @private
*/
_restoreEditingProps: function() {
if (!this._savedProps) {
return;
}
this.hoverCursor = this._savedProps.overCursor;
this.hasControls = this._savedProps.hasControls;
this.borderColor = this._savedProps.borderColor;
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.selectable = true;
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;
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];
}
}
},
/**
* @private
*/
_removeCharsFromTo: function(start, end) {
while (end !== start) {
this._removeSingleCharAndStyle(start + 1);
end--;
}
this.selectionStart = start;
this.selectionEnd = start;
},
_removeSingleCharAndStyle: function(index) {
var isBeginningOfLine = this.text[index - 1] === '\n',
indexStyle = isBeginningOfLine ? index : index - 1;
this.removeStyleObject(isBeginningOfLine, indexStyle);
this.text = this.text.slice(0, index - 1) +
this.text.slice(index);
this._textLines = this._splitTextIntoLines();
},
/**
* Inserts characters where cursor is (replacing selection if one exists)
* @param {String} _chars Characters to insert
* @param {Boolean} useCopiedStyle use fabric.copiedTextStyle
*/
insertChars: function(_chars, useCopiedStyle) {
var style;
if (this.selectionEnd - this.selectionStart > 1) {
this._removeCharsFromTo(this.selectionStart, this.selectionEnd);
}
//short circuit for block paste
if (!useCopiedStyle && this.isEmptyStyles()) {
this.insertChar(_chars, false);
return;
}
for (var i = 0, len = _chars.length; i < len; i++) {
if (useCopiedStyle) {
style = fabric.util.object.clone(fabric.copiedTextStyle[i], true);
}
this.insertChar(_chars[i], i < len - 1, style);
}
},
/**
* Inserts a character where cursor is
* @param {String} _char Characters to insert
* @param {Boolean} skipUpdate trigger rendering and updates at the end of text insert
* @param {Object} styleObject Style to be inserted for the new char
*/
insertChar: function(_char, skipUpdate, styleObject) {
var isEndOfLine = this.text[this.selectionStart] === '\n';
this.text = this.text.slice(0, this.selectionStart) +
_char + this.text.slice(this.selectionEnd);
this._textLines = this._splitTextIntoLines();
this.insertStyleObjects(_char, isEndOfLine, styleObject);
this.selectionStart += _char.length;
this.selectionEnd = this.selectionStart;
if (skipUpdate) {
return;
}
this._updateTextarea();
this.setCoords();
this._fireSelectionChanged();
this.fire('changed');
this.restartCursorIfNeeded();
if (this.canvas) {
this.canvas.fire('text:changed', { target: this });
this.canvas.renderAll();
}
},
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 {Boolean} isEndOfLine True if it's end of line
*/
insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) {
this.shiftLineStyles(lineIndex, +1);
var currentCharStyle = {},
newLineStyles = {};
if (this.styles[lineIndex] && this.styles[lineIndex][charIndex - 1]) {
currentCharStyle = this.styles[lineIndex][charIndex - 1];
}
// if there's nothing after cursor,
// we clone current char style onto the next (otherwise empty) line
if (isEndOfLine && currentCharStyle) {
newLineStyles[0] = clone(currentCharStyle);
this.styles[lineIndex + 1] = newLineStyles;
}
// otherwise we clone styles of all chars
// after cursor onto the next line, from the beginning
else {
var somethingAdded = false;
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];
}
}
somethingAdded && (this.styles[lineIndex + 1] = newLineStyles);
}
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 {Object} [style] Style object to insert, if given
*/
insertCharStyleObject: function(lineIndex, charIndex, style) {
var currentLineStyles = this.styles[lineIndex],
currentLineStylesCloned = clone(currentLineStyles);
if (charIndex === 0 && !style) {
charIndex = 1;
}
// shift all char styles by 1 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 + 1] = currentLineStylesCloned[numericIndex];
// only delete the style if there was nothing moved there
if (!currentLineStylesCloned[numericIndex - 1]) {
delete currentLineStyles[numericIndex];
}
}
}
var newStyle = style || clone(currentLineStyles[charIndex - 1]);
newStyle && (this.styles[lineIndex][charIndex] = newStyle);
this._forceClearCache = true;
},
/**
* Inserts style object(s)
* @param {String} _chars Characters at the location where style is inserted
* @param {Boolean} isEndOfLine True if it's end of line
* @param {Object} [styleObject] Style to insert
*/
insertStyleObjects: function(_chars, isEndOfLine, styleObject) {
// removed shortcircuit over isEmptyStyles
var cursorLocation = this.get2DCursorLocation(),
lineIndex = cursorLocation.lineIndex,
charIndex = cursorLocation.charIndex;
if (!this._getLineStyle(lineIndex)) {
this._setLineStyle(lineIndex, {});
}
if (_chars === '\n') {
this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine);
}
else {
this.insertCharStyleObject(lineIndex, charIndex, styleObject);
}
},
/**
* Shifts line styles up or down
* @param {Number} lineIndex Index of a line
* @param {Number} offset Can be -1 or +1
*/
shiftLineStyles: function(lineIndex, offset) {
// shift all line styles by 1 upward or downward
var clonedStyles = clone(this.styles);
for (var line in clonedStyles) {
var numericLine = parseInt(line, 10);
if (numericLine <= lineIndex) {
delete clonedStyles[numericLine];
}
}
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];
}
}
}
//TODO: evaluate if delete old style lines with offset -1
},
/**
* Removes style object
* @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line
* @param {Number} [index] Optional index. When not given, current selectionStart is used.
*/
removeStyleObject: function(isBeginningOfLine, index) {
var cursorLocation = this.get2DCursorLocation(index),
lineIndex = cursorLocation.lineIndex,
charIndex = cursorLocation.charIndex;
this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex);
},
_getTextOnPreviousLine: function(lIndex) {
return this._textLines[lIndex - 1];
},
_removeStyleObject: function(isBeginningOfLine, cursorLocation, lineIndex, charIndex) {
if (isBeginningOfLine) {
var textOnPreviousLine = this._getTextOnPreviousLine(cursorLocation.lineIndex),
newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0;
if (!this.styles[lineIndex - 1]) {
this.styles[lineIndex - 1] = {};
}
for (charIndex in this.styles[lineIndex]) {
this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine]
= this.styles[lineIndex][charIndex];
}
this.shiftLineStyles(cursorLocation.lineIndex, -1);
}
else {
var currentLineStyles = this.styles[lineIndex];
if (currentLineStyles) {
delete currentLineStyles[charIndex];
}
var currentLineStylesCloned = clone(currentLineStyles);
// shift all styles by 1 backwards
for (var i in currentLineStylesCloned) {
var numericIndex = parseInt(i, 10);
if (numericIndex >= charIndex && numericIndex !== 0) {
currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex];
delete currentLineStyles[numericIndex];
}
}
}
},
/**
* Inserts new line
*/
insertNewline: function() {
this.insertChars('\n');
},
/**
* Set the selectionStart and selectionEnd according to the ne postion 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.bind(this));
},
onMouseDown: function(options) {
this.__newClickTime = +new Date();
var newPointer = this.canvas.getPointer(options.e);
if (this.isTripleClick(newPointer)) {
this.fire('tripleclick', options);
this._stopEvent(options.e);
}
else if (this.isDoubleClick(newPointer)) {
this.fire('dblclick', options);
this._stopEvent(options.e);
}
this.__lastLastClickTime = this.__lastClickTime;
this.__lastClickTime = this.__newClickTime;
this.__lastPointer = newPointer;
this.__lastIsEditing = this.isEditing;
this.__lastSelected = this.selected;
},
isDoubleClick: function(newPointer) {
return this.__newClickTime - this.__lastClickTime < 500 &&
this.__lastPointer.x === newPointer.x &&
this.__lastPointer.y === newPointer.y && this.__lastIsEditing;
},
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('dblclick', function(options) {
this.selectWord(this.getSelectionStartFromPointer(options.e));
});
this.on('tripleclick', function(options) {
this.selectLine(this.getSelectionStartFromPointer(options.e));
});
},
/**
* Initializes "mousedown" event handler
*/
initMousedownHandler: function() {
this.on('mousedown', function(options) {
if (!this.editable) {
return;
}
var pointer = this.canvas.getPointer(options.e);
this.__mousedownX = pointer.x;
this.__mousedownY = pointer.y;
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();
}
});
},
/**
* @private
*/
_isObjectMoved: function(e) {
var pointer = this.canvas.getPointer(e);
return this.__mousedownX !== pointer.x ||
this.__mousedownY !== pointer.y;
},
/**
* Initializes "mouseup" event handler
*/
initMouseupHandler: function() {
this.on('mouseup', function(options) {
this.__isMousedown = false;
if (!this.editable || this._isObjectMoved(options.e)) {
return;
}
if (this.__lastSelected && !this.__corner) {
this.enterEditing(options.e);
if (this.selectionStart === this.selectionEnd) {
this.initDelayedCursor(true);
}
else {
this.renderCursorOrSelection();
}
}
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,
newSelectionStart,
line;
for (var i = 0, len = this._textLines.length; i < len; i++) {
line = this._textLines[i];
height += this._getHeightOfLine(this.ctx, i) * this.scaleY;
var widthOfLine = this._getLineWidth(this.ctx, i),
lineLeftOffset = this._getLineLeftOffset(widthOfLine);
width = lineLeftOffset * this.scaleX;
for (var j = 0, jlen = line.length; j < jlen; j++) {
prevWidth = width;
width += this._getWidthOfChar(this.ctx, line[j], i, this.flipX ? jlen - j : j) *
this.scaleX;
if (height <= mouseOffset.y || width <= mouseOffset.x) {
charIndex++;
continue;
}
return this._getNewSelectionStartFromOffset(
mouseOffset, prevWidth, width, charIndex + i, jlen);
}
if (mouseOffset.y < height) {
//this happens just on end of lines.
return this._getNewSelectionStartFromOffset(
mouseOffset, prevWidth, width, charIndex + i - 1, jlen);
}
}
// clicked somewhere after all chars, so set at the end
if (typeof newSelectionStart === 'undefined') {
return this.text.length;
}
},
/**
* @private
*/
_getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
distanceBtwNextCharAndCursor = width - mouseOffset.x,
offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 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');
var style = this._calcTextareaPosition();
this.hiddenTextarea.style.cssText = 'white-space: nowrap; position: absolute; top: ' + style.top +
'; left: ' + style.left + '; opacity: 0; width: 1px; height: 1px; z-index: -999;';
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.cut.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;
}
},
/**
* @private
*/
_keysMap: {
8: 'removeChars',
9: 'exitEditing',
27: 'exitEditing',
13: 'insertNewline',
33: 'moveCursorUp',
34: 'moveCursorDown',
35: 'moveCursorRight',
36: 'moveCursorLeft',
37: 'moveCursorLeft',
38: 'moveCursorUp',
39: 'moveCursorRight',
40: 'moveCursorDown',
46: 'forwardDelete'
},
/**
* @private
*/
_ctrlKeysMapUp: {
67: 'copy',
88: 'cut'
},
/**
* @private
*/
_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) {
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.renderAll();
}
},
/**
* 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._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.renderAll();
},
/**
* Handles onInput event
* @param {Event} e Event object
*/
onInput: function(e) {
if (!this.isEditing || this.inCompositionMode) {
return;
}
var offset = this.selectionStart || 0,
offsetEnd = this.selectionEnd || 0,
textLength = this.text.length,
newTextLength = this.hiddenTextarea.value.length,
diff, charsToInsert, start;
if (newTextLength > textLength) {
//we added some character
start = this._selectionDirection === 'left' ? offsetEnd : offset;
diff = newTextLength - textLength;
charsToInsert = this.hiddenTextarea.value.slice(start, start + diff);
}
else {
//we selected a portion of text and then input something else.
//Internet explorer does not trigger this else
diff = newTextLength - textLength + offsetEnd - offset;
charsToInsert = this.hiddenTextarea.value.slice(offset, offset + diff);
}
this.insertChars(charsToInsert);
e.stopPropagation();
},
/**
* Composition start
*/
onCompositionStart: function() {
this.inCompositionMode = true;
this.prevCompositionLength = 0;
this.compositionStart = this.selectionStart;
},
/**
* Composition end
*/
onCompositionEnd: function() {
this.inCompositionMode = false;
},
/**
* Composition update
*/
onCompositionUpdate: function(e) {
var data = e.data;
this.selectionStart = this.compositionStart;
this.selectionEnd = this.selectionEnd === this.selectionStart ?
this.compositionStart + this.prevCompositionLength : this.selectionEnd;
this.insertChars(data, false);
this.prevCompositionLength = data.length;
},
/**
* Forward delete
*/
forwardDelete: function(e) {
if (this.selectionStart === this.selectionEnd) {
if (this.selectionStart === this.text.length) {
return;
}
this.moveCursorRight(e);
}
this.removeChars(e);
},
/**
* Copies selected text
* @param {Event} e Event object
*/
copy: function(e) {
if (this.selectionStart === this.selectionEnd) {
//do not cut-copy if no selection
return;
}
var selectedText = this.getSelectedText(),
clipboardData = this._getClipboardData(e);
// Check for backward compatibility with old browsers
if (clipboardData) {
clipboardData.setData('text', selectedText);
}
fabric.copiedText = selectedText;
fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd);
e.stopImmediatePropagation();
e.preventDefault();
this._copyDone = true;
},
/**
* Pastes text
* @param {Event} e Event object
*/
paste: function(e) {
var copiedText = null,
clipboardData = this._getClipboardData(e),
useCopiedStyle = true;
// Check for backward compatibility with old browsers
if (clipboardData) {
copiedText = clipboardData.getData('text').replace(/\r/g, '');
if (!fabric.copiedTextStyle || fabric.copiedText !== copiedText) {
useCopiedStyle = false;
}
}
else {
copiedText = fabric.copiedText;
}
if (copiedText) {
this.insertChars(copiedText, useCopiedStyle);
}
e.stopImmediatePropagation();
e.preventDefault();
},
/**
* Cuts text
* @param {Event} e Event object
*/
cut: function(e) {
if (this.selectionStart === this.selectionEnd) {
return;
}
this.copy(e);
this.removeChars(e);
},
/**
* @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 textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex),
widthOfLine = this._getLineWidth(this.ctx, lineIndex),
widthBeforeCursor = this._getLineLeftOffset(widthOfLine), _char;
for (var i = 0, len = textBeforeCursor.length; i < len; i++) {
_char = textBeforeCursor[i];
widthBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);
}
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 + 2;
},
/**
* 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);
// return a negative offset
return -this._textLines[lineIndex - 1].length + indexOnOtherLine - textBeforeCursor.length;
},
/**
* find for a given width it founds the matching character.
* @private
*/
_getIndexOnLine: function(lineIndex, width) {
var widthOfLine = this._getLineWidth(this.ctx, lineIndex),
textOnLine = this._textLines[lineIndex],
lineLeftOffset = this._getLineLeftOffset(widthOfLine),
widthOfCharsOnLine = lineLeftOffset,
indexOnLine = 0,
foundMatch;
for (var j = 0, jlen = textOnLine.length; j < jlen; j++) {
var _char = textOnLine[j],
widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);
widthOfCharsOnLine += widthOfChar;
if (widthOfCharsOnLine > width) {
foundMatch = true;
var leftEdge = widthOfCharsOnLine - widthOfChar,
rightEdge = widthOfCharsOnLine,
offsetFromLeftEdge = Math.abs(leftEdge - width),
offsetFromRightEdge = Math.abs(rightEdge - width);
indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
break;
}
}
// reached end
if (!foundMatch) {
indexOnLine = textOnLine.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 selected by selection
* @param {Event} e Event object
*/
removeChars: function(e) {
if (this.selectionStart === this.selectionEnd) {
this._removeCharsNearCursor(e);
}
else {
this._removeCharsFromTo(this.selectionStart, this.selectionEnd);
}
this.set('dirty', true);
this.setSelectionEnd(this.selectionStart);
this._removeExtraneousStyles();
this.canvas && this.canvas.renderAll();
this.setCoords();
this.fire('changed');
this.canvas && this.canvas.fire('text:changed', { target: this });
},
/**
* @private
* @param {Event} e Event object
*/
_removeCharsNearCursor: function(e) {
if (this.selectionStart === 0) {
return;
}
if (e.metaKey) {
// remove all till the start of current line
var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart);
this._removeCharsFromTo(leftLineBoundary, this.selectionStart);
this.setSelectionStart(leftLineBoundary);
}
else if (e.altKey) {
// remove all till the start of current word
var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart);
this._removeCharsFromTo(leftWordBoundary, this.selectionStart);
this.setSelectionStart(leftWordBoundary);
}
else {
this._removeSingleCharAndStyle(this.selectionStart);
this.setSelectionStart(this.selectionStart - 1);
}
}
});
/* _TO_SVG_START_ */
(function() {
var toFixed = fabric.util.toFixed,
NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
/**
* @private
*/
_setSVGTextLineText: function(lineIndex, textSpans, height, textLeftOffset, textTopOffset, textBgRects) {
if (!this._getLineStyle(lineIndex)) {
fabric.Text.prototype._setSVGTextLineText.call(this,
lineIndex, textSpans, height, textLeftOffset, textTopOffset);
}
else {
this._setSVGTextLineChars(
lineIndex, textSpans, height, textLeftOffset, textBgRects);
}
},
/**
* @private
*/
_setSVGTextLineChars: function(lineIndex, textSpans, height, textLeftOffset, textBgRects) {
var chars = this._textLines[lineIndex],
charOffset = 0,
lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)) - this.width / 2,
lineOffset = this._getSVGLineTopOffset(lineIndex),
heightOfLine = this._getHeightOfLine(this.ctx, lineIndex);
for (var i = 0, len = chars.length; i < len; i++) {
var styleDecl = this._getStyleDeclaration(lineIndex, i) || { };
textSpans.push(
this._createTextCharSpan(
chars[i], styleDecl, lineLeftOffset, lineOffset.lineTop + lineOffset.offset, charOffset));
var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i);
if (styleDecl.textBackgroundColor) {
textBgRects.push(
this._createTextCharBg(
styleDecl, lineLeftOffset, lineOffset.lineTop, heightOfLine, charWidth, charOffset));
}
charOffset += charWidth;
}
},
/**
* @private
*/
_getSVGLineTopOffset: function(lineIndex) {
var lineTopOffset = 0, lastHeight = 0;
for (var j = 0; j < lineIndex; j++) {
lineTopOffset += this._getHeightOfLine(this.ctx, j);
}
lastHeight = this._getHeightOfLine(this.ctx, j);
return {
lineTop: lineTopOffset,
offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
};
},
/**
* @private
*/
_createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) {
return [
'\t\t\n'
].join('');
},
/**
* @private
*/
_createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, charOffset) {
var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({
visible: true,
fill: this.fill,
stroke: this.stroke,
type: 'text',
getSvgFilter: fabric.Object.prototype.getSvgFilter
}, styleDecl));
return [
'\t\t\t',
fabric.util.string.escapeXml(_char),
'\n'
].join('');
}
});
})();
/* _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
*/
lockScalingY: true,
/**
* Override standard Object class values
*/
lockScalingFlip: true,
/**
* Override standard Object class values
* Textbox needs this on false
*/
noScaleCache: false,
/**
* Constructor. Some scaling related property values are forced. Visibility
* of controls is also fixed; only the rotation and width controls are
* made available.
* @param {String} text Text string
* @param {Object} [options] Options object
* @return {fabric.Textbox} thisArg
*/
initialize: function(text, options) {
this.callSuper('initialize', text, options);
this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility());
this.ctx = this.objectCaching ? this._cacheContext : fabric.util.createCanvasElement().getContext('2d');
// add width to this list of props that effect line wrapping.
this._dimensionAffectingProps.push('width');
},
/**
* Unlike superclass's version of this function, Textbox does not update
* its width.
* @param {CanvasRenderingContext2D} ctx Context to use for measurements
* @private
* @override
*/
_initDimensions: function(ctx) {
if (this.__skipDimension) {
return;
}
if (!ctx) {
ctx = fabric.util.createCanvasElement().getContext('2d');
this._setTextStyles(ctx);
this.clearContextTop();
}
// clear dynamicMinWidth as it will be different after we re-wrap line
this.dynamicMinWidth = 0;
// wrap lines
this._textLines = this._splitTextIntoLines(ctx);
// 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);
}
// clear cache and re-calculate height
this._clearCache();
this.height = this._getTextHeight(ctx);
},
/**
* 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() {
var realLineCount = 0,
realLineCharCount = 0,
charCount = 0,
map = {};
for (var i = 0; i < this._textLines.length; i++) {
if (this.text[charCount] === '\n' && i > 0) {
realLineCharCount = 0;
charCount++;
realLineCount++;
}
else if (this.text[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 += this._textLines[i].length;
realLineCharCount += this._textLines[i].length;
}
return map;
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {Boolean} [returnCloneOrEmpty=false]
* @private
*/
_getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) {
if (this._styleMap) {
var map = this._styleMap[lineIndex];
if (!map) {
return returnCloneOrEmpty ? { } : null;
}
lineIndex = map.line;
charIndex = map.offset + charIndex;
}
return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty);
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {Object} style
* @private
*/
_setStyleDeclaration: function(lineIndex, charIndex, style) {
var map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
this.styles[lineIndex][charIndex] = style;
},
/**
* @param {Number} lineIndex
* @param {Number} charIndex
* @private
*/
_deleteStyleDeclaration: function(lineIndex, charIndex) {
var map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
delete this.styles[lineIndex][charIndex];
},
/**
* @param {Number} lineIndex
* @private
*/
_getLineStyle: function(lineIndex) {
var map = this._styleMap[lineIndex];
return this.styles[map.line];
},
/**
* @param {Number} lineIndex
* @param {Object} style
* @private
*/
_setLineStyle: function(lineIndex, style) {
var map = this._styleMap[lineIndex];
this.styles[map.line] = style;
},
/**
* @param {Number} lineIndex
* @private
*/
_deleteLineStyle: function(lineIndex) {
var map = this._styleMap[lineIndex];
delete this.styles[map.line];
},
/**
* Wraps text using the 'width' property of Textbox. First this function
* splits text on newlines, so we preserve newlines entered by the user.
* Then it wraps each line using the width of the Textbox by calling
* _wrapLine().
* @param {CanvasRenderingContext2D} ctx Context to use for measurements
* @param {String} text The string of text that is split into lines
* @returns {Array} Array of lines
*/
_wrapText: function(ctx, text) {
var lines = text.split(this._reNewline), wrapped = [], i;
for (i = 0; i < lines.length; i++) {
wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i));
}
return wrapped;
},
/**
* Helper function to measure a string of text, given its lineIndex and charIndex offset
*
* @param {CanvasRenderingContext2D} ctx
* @param {String} text
* @param {number} lineIndex
* @param {number} charOffset
* @returns {number}
* @private
*/
_measureText: function(ctx, text, lineIndex, charOffset) {
var width = 0;
charOffset = charOffset || 0;
for (var i = 0, len = text.length; i < len; i++) {
width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset);
}
return width;
},
/**
* Wraps a line of text using the width of the Textbox and a context.
* @param {CanvasRenderingContext2D} ctx Context to use for measurements
* @param {String} text The string of text to split into lines
* @param {Number} lineIndex
* @returns {Array} Array of line(s) into which the given text is wrapped
* to.
*/
_wrapLine: function(ctx, text, lineIndex) {
var lineWidth = 0,
lines = [],
line = '',
words = text.split(' '),
word = '',
offset = 0,
infix = ' ',
wordWidth = 0,
infixWidth = 0,
largestWordWidth = 0,
lineJustStarted = true,
additionalSpace = this._getWidthOfCharSpacing();
for (var i = 0; i < words.length; i++) {
word = words[i];
wordWidth = this._measureText(ctx, word, lineIndex, offset);
offset += word.length;
lineWidth += infixWidth + wordWidth - additionalSpace;
if (lineWidth >= this.width && !lineJustStarted) {
lines.push(line);
line = '';
lineWidth = wordWidth;
lineJustStarted = true;
}
else {
lineWidth += additionalSpace;
}
if (!lineJustStarted) {
line += infix;
}
line += word;
infixWidth = this._measureText(ctx, infix, lineIndex, offset);
offset++;
lineJustStarted = false;
// keep track of largest word
if (wordWidth > largestWordWidth) {
largestWordWidth = wordWidth;
}
}
i && lines.push(line);
if (largestWordWidth > this.dynamicMinWidth) {
this.dynamicMinWidth = largestWordWidth - additionalSpace;
}
return lines;
},
/**
* Gets lines of text to render in the Textbox. This function calculates
* text wrapping on the fly everytime it is called.
* @returns {Array} Array of lines in the Textbox.
* @override
*/
_splitTextIntoLines: function(ctx) {
ctx = ctx || this.ctx;
var originalAlign = this.textAlign;
this._styleMap = null;
ctx.save();
this._setTextStyles(ctx);
this.textAlign = 'left';
var lines = this._wrapText(ctx, this.text);
this.textAlign = originalAlign;
ctx.restore();
this._textLines = lines;
this._styleMap = this._generateStyleMap();
return lines;
},
/**
* When part of a group, we don't want the Textbox's scale to increase if
* the group's increases. That's why we reduce the scale of the Textbox by
* the amount that the group's increases. This is to maintain the effective
* scale of the Textbox at 1, so that font-size values make sense. Otherwise
* the same font-size value would result in different actual size depending
* on the value of the scale.
* @param {String} key
* @param {*} value
*/
setOnGroup: function(key, value) {
if (key === 'scaleX') {
this.set('scaleX', Math.abs(1 / value));
this.set('width', (this.get('width') * value) /
(typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX));
this.__oldScaleX = value;
}
},
/**
* Returns 2d representation (lineIndex and charIndex) of cursor (or selection start).
* Overrides the superclass function to take into account text wrapping.
*
* @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
*/
get2DCursorLocation: function(selectionStart) {
if (typeof selectionStart === 'undefined') {
selectionStart = this.selectionStart;
}
var numLines = this._textLines.length,
removed = 0;
for (var i = 0; i < numLines; i++) {
var line = this._textLines[i],
lineLen = line.length;
if (selectionStart <= removed + lineLen) {
return {
lineIndex: i,
charIndex: selectionStart - removed
};
}
removed += lineLen;
if (this.text[removed] === '\n' || this.text[removed] === ' ') {
removed++;
}
}
return {
lineIndex: numLines - 1,
charIndex: this._textLines[numLines - 1].length
};
},
/**
* Overrides superclass function and uses text wrapping data to get cursor
* boundary offsets instead of the array of chars.
* @param {Array} chars Unused
* @param {String} typeOfBoundaries Can be 'cursor' or 'selection'
* @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set.
*/
_getCursorBoundariesOffsets: function(chars, typeOfBoundaries) {
var topOffset = 0,
leftOffset = 0,
cursorLocation = this.get2DCursorLocation(),
lineChars = this._textLines[cursorLocation.lineIndex].split(''),
lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex));
for (var i = 0; i < cursorLocation.charIndex; i++) {
leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i);
}
for (i = 0; i < cursorLocation.lineIndex; i++) {
topOffset += this._getHeightOfLine(this.ctx, i);
}
if (typeOfBoundaries === 'cursor') {
topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex)
/ this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)
* (1 - this._fontSizeFraction);
}
return {
top: topOffset,
left: leftOffset,
lineLeft: lineLeftOffset
};
},
getMinWidth: function() {
return Math.max(this.minWidth, this.dynamicMinWidth);
},
/**
* Returns object representation of an instance
* @method toObject
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return this.callSuper('toObject', ['minWidth'].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
* @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first
* @return {fabric.Textbox} instance of fabric.Textbox
*/
fabric.Textbox.fromObject = function(object, callback, forceAsync) {
return fabric.Object._fromObject('Textbox', object, callback, forceAsync, 'text');
};
/**
* Returns the default controls visibility required for Textboxes.
* @returns {Object}
*/
fabric.Textbox.getTextboxControlVisibility = function() {
return {
tl: false,
tr: false,
br: false,
bl: false,
ml: true,
mt: false,
mr: true,
mb: false,
mtr: true
};
};
})(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;
if (t instanceof fabric.Textbox) {
var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth));
if (w >= t.getMinWidth()) {
t.set('width', w);
return true;
}
}
else {
return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform,
lockScalingX, lockScalingY, by, lockScalingFlip, _dim);
}
};
/**
* Sets controls of this group to the Textbox's special configuration if
* one is present in the group. Deletes _controlsVisibility otherwise, so that
* it gets initialized to default value at runtime.
*/
fabric.Group.prototype._refreshControlsVisibility = function() {
if (typeof fabric.Textbox === 'undefined') {
return;
}
for (var i = this._objects.length; i--;) {
if (this._objects[i] instanceof fabric.Textbox) {
this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility());
return;
}
}
};
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];
}
}
},
/**
* Inserts style object for a given line/char index
* @param {Number} lineIndex Index of a line
* @param {Number} charIndex Index of a char
* @param {Object} [style] Style object to insert, if given
*/
insertCharStyleObject: function(lineIndex, charIndex, style) {
// adjust lineIndex and charIndex
var map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
fabric.IText.prototype.insertCharStyleObject.apply(this, [lineIndex, charIndex, style]);
},
/**
* Inserts new style object
* @param {Number} lineIndex Index of a line
* @param {Number} charIndex Index of a char
* @param {Boolean} isEndOfLine True if it's end of line
*/
insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) {
// adjust lineIndex and charIndex
var map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
fabric.IText.prototype.insertNewlineStyleObject.apply(this, [lineIndex, charIndex, isEndOfLine]);
},
/**
* Shifts line styles up or down. This function is slightly different than the one in
* itext_behaviour as it takes into account the styleMap.
*
* @param {Number} lineIndex Index of a line
* @param {Number} offset Can be -1 or +1
*/
shiftLineStyles: function(lineIndex, offset) {
// shift all line styles by 1 upward
var map = this._styleMap[lineIndex];
// adjust line index
lineIndex = map.line;
fabric.IText.prototype.shiftLineStyles.call(this, lineIndex, offset);
},
/**
* Figure out programatically the text on previous actual line (actual = separated by \n);
*
* @param {Number} lIndex
* @returns {String}
* @private
*/
_getTextOnPreviousLine: function(lIndex) {
var textOnPreviousLine = this._textLines[lIndex - 1];
while (this._styleMap[lIndex - 2] && this._styleMap[lIndex - 2].line === this._styleMap[lIndex - 1].line) {
textOnPreviousLine = this._textLines[lIndex - 2] + textOnPreviousLine;
lIndex--;
}
return textOnPreviousLine;
},
/**
* Removes style object
* @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line
* @param {Number} [index] Optional index. When not given, current selectionStart is used.
*/
removeStyleObject: function(isBeginningOfLine, index) {
var cursorLocation = this.get2DCursorLocation(index),
map = this._styleMap[cursorLocation.lineIndex],
lineIndex = map.line,
charIndex = map.offset + cursorLocation.charIndex;
this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex);
}
});
})();
(function() {
var override = fabric.IText.prototype._getNewSelectionStartFromOffset;
/**
* Overrides the IText implementation and adjusts character index as there is not always a linebreak
*
* @param {Number} mouseOffset
* @param {Number} prevWidth
* @param {Number} width
* @param {Number} index
* @param {Number} jlen
* @returns {Number}
*/
fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, prevWidth, width, index, jlen) {
index = override.call(this, mouseOffset, prevWidth, width, index, jlen);
// the index passed into the function is padded by the amount of lines from _textLines (to account for \n)
// we need to remove this padding, and pad it by actual lines, and / or spaces that are meant to be there
var tmp = 0,
removed = 0;
// account for removed characters
for (var i = 0; i < this._textLines.length; i++) {
tmp += this._textLines[i].length;
if (tmp + removed >= index) {
break;
}
if (this.text[tmp + removed] === '\n' || this.text[tmp + removed] === ' ') {
removed++;
}
}
return index - i + removed;
};
})();
(function() {
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
return;
}
var DOMParser = require('xmldom').DOMParser,
URL = require('url'),
HTTP = require('http'),
HTTPS = require('https'),
Canvas = require('canvas'),
Image = require('canvas').Image;
/** @private */
function request(url, encoding, callback) {
var oURL = URL.parse(url);
// detect if http or https is used
if ( !oURL.port ) {
oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80;
}
// assign request handler based on protocol
var reqHandler = (oURL.protocol.indexOf('https:') === 0 ) ? HTTPS : HTTP,
req = reqHandler.request({
hostname: oURL.hostname,
port: oURL.port,
path: oURL.path,
method: 'GET'
}, function(response) {
var body = '';
if (encoding) {
response.setEncoding(encoding);
}
response.on('end', function () {
callback(body);
});
response.on('data', function (chunk) {
if (response.statusCode === 200) {
body += chunk;
}
});
});
req.on('error', function(err) {
if (err.errno === process.ECONNREFUSED) {
fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port);
}
else {
fabric.log(err.message);
}
callback(null);
});
req.end();
}
/** @private */
function requestFs(path, callback) {
var fs = require('fs');
fs.readFile(path, function (err, data) {
if (err) {
fabric.log(err);
throw err;
}
else {
callback(data);
}
});
}
fabric.util.loadImage = function(url, callback, context) {
function createImageAndCallBack(data) {
if (data) {
img.src = new Buffer(data, 'binary');
// preserving original url, which seems to be lost in node-canvas
img._src = url;
callback && callback.call(context, img);
}
else {
img = null;
callback && callback.call(context, null, true);
}
}
var img = new Image();
if (url && (url instanceof Buffer || url.indexOf('data') === 0)) {
img.src = img._src = url;
callback && callback.call(context, img);
}
else if (url && url.indexOf('http') !== 0) {
requestFs(url, createImageAndCallBack);
}
else if (url) {
request(url, 'binary', createImageAndCallBack);
}
else {
callback && callback.call(context, url);
}
};
fabric.loadSVGFromURL = function(url, callback, reviver) {
url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim();
if (url.indexOf('http') !== 0) {
requestFs(url, function(body) {
fabric.loadSVGFromString(body.toString(), callback, reviver);
});
}
else {
request(url, '', function(body) {
fabric.loadSVGFromString(body, callback, reviver);
});
}
};
fabric.loadSVGFromString = function(string, callback, reviver) {
var doc = new DOMParser().parseFromString(string);
fabric.parseSVGDocument(doc.documentElement, function(results, options) {
callback && callback(results, options);
}, reviver);
};
fabric.util.getScript = function(url, callback) {
request(url, '', function(body) {
// eslint-disable-next-line no-eval
eval(body);
callback && callback();
});
};
// fabric.util.createCanvasElement = function(_, width, height) {
// return new Canvas(width, height);
// }
/**
* Only available when running fabric on node.js
* @param {Number} width Canvas width
* @param {Number} height Canvas height
* @param {Object} [options] Options to pass to FabricCanvas.
* @param {Object} [nodeCanvasOptions] Options to pass to NodeCanvas.
* @return {Object} wrapped canvas instance
*/
fabric.createCanvasForNode = function(width, height, options, nodeCanvasOptions) {
nodeCanvasOptions = nodeCanvasOptions || options;
var canvasEl = fabric.document.createElement('canvas'),
nodeCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions),
nodeCacheCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions);
// jsdom doesn't create style on canvas element, so here be temp. workaround
canvasEl.style = { };
canvasEl.width = nodeCanvas.width;
canvasEl.height = nodeCanvas.height;
options = options || { };
options.nodeCanvas = nodeCanvas;
options.nodeCacheCanvas = nodeCacheCanvas;
var FabricCanvas = fabric.Canvas || fabric.StaticCanvas,
fabricCanvas = new FabricCanvas(canvasEl, options);
fabricCanvas.nodeCanvas = nodeCanvas;
fabricCanvas.nodeCacheCanvas = nodeCacheCanvas;
fabricCanvas.contextContainer = nodeCanvas.getContext('2d');
fabricCanvas.contextCache = nodeCacheCanvas.getContext('2d');
fabricCanvas.Font = Canvas.Font;
return fabricCanvas;
};
var originaInitStatic = fabric.StaticCanvas.prototype._initStatic;
fabric.StaticCanvas.prototype._initStatic = function(el, options) {
el = el || fabric.document.createElement('canvas');
this.nodeCanvas = new Canvas(el.width, el.height);
this.nodeCacheCanvas = new Canvas(el.width, el.height);
originaInitStatic.call(this, el, options);
this.contextContainer = this.nodeCanvas.getContext('2d');
this.contextCache = this.nodeCacheCanvas.getContext('2d');
this.Font = Canvas.Font;
};
/** @ignore */
fabric.StaticCanvas.prototype.createPNGStream = function() {
return this.nodeCanvas.createPNGStream();
};
fabric.StaticCanvas.prototype.createJPEGStream = function(opts) {
return this.nodeCanvas.createJPEGStream(opts);
};
fabric.StaticCanvas.prototype._initRetinaScaling = function() {
if (!this._isRetinaScaling()) {
return;
}
this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio);
this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio);
this.nodeCanvas.width = this.width * fabric.devicePixelRatio;
this.nodeCanvas.height = this.height * fabric.devicePixelRatio;
this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio);
return this;
};
if (fabric.Canvas) {
fabric.Canvas.prototype._initRetinaScaling = fabric.StaticCanvas.prototype._initRetinaScaling;
}
var origSetBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension;
fabric.StaticCanvas.prototype._setBackstoreDimension = function(prop, value) {
origSetBackstoreDimension.call(this, prop, value);
this.nodeCanvas[prop] = value;
return this;
};
if (fabric.Canvas) {
fabric.Canvas.prototype._setBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension;
}
})();