mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-05-26 22:14:01 +00:00
511 lines
15 KiB
JavaScript
511 lines
15 KiB
JavaScript
(function() {
|
|
|
|
function parseDecoration(object) {
|
|
if (object.textDecoration) {
|
|
object.textDecoration.indexOf('underline') > -1 && (object.underline = true);
|
|
object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true);
|
|
object.textDecoration.indexOf('overline') > -1 && (object.overline = true);
|
|
delete object.textDecoration;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* IText class (introduced in <b>v1.4</b>) Events are also fired with "text:"
|
|
* prefix when observing canvas.
|
|
* @class fabric.IText
|
|
* @extends fabric.Text
|
|
* @mixes fabric.Observable
|
|
*
|
|
* @fires changed
|
|
* @fires selection:changed
|
|
* @fires editing:entered
|
|
* @fires editing:exited
|
|
*
|
|
* @return {fabric.IText} thisArg
|
|
* @see {@link fabric.IText#initialize} for constructor definition
|
|
*
|
|
* <p>Supported key combinations:</p>
|
|
* <pre>
|
|
* Move cursor: left, right, up, down
|
|
* Select character: shift + left, shift + right
|
|
* Select text vertically: shift + up, shift + down
|
|
* Move cursor by word: alt + left, alt + right
|
|
* Select words: shift + alt + left, shift + alt + right
|
|
* Move cursor to line start/end: cmd + left, cmd + right or home, end
|
|
* Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end
|
|
* Jump to start/end of text: cmd + up, cmd + down
|
|
* Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
|
|
* Delete character: backspace
|
|
* Delete word: alt + backspace
|
|
* Delete line: cmd + backspace
|
|
* Forward delete: delete
|
|
* Copy text: ctrl/cmd + c
|
|
* Paste text: ctrl/cmd + v
|
|
* Cut text: ctrl/cmd + x
|
|
* Select entire text: ctrl/cmd + a
|
|
* Quit editing tab or esc
|
|
* </pre>
|
|
*
|
|
* <p>Supported mouse/touch combination</p>
|
|
* <pre>
|
|
* Position cursor: click/touch
|
|
* Create selection: click/touch & drag
|
|
* Create selection: click & shift + click
|
|
* Select word: double click
|
|
* Select line: triple click
|
|
* </pre>
|
|
*/
|
|
fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ {
|
|
|
|
/**
|
|
* Type of an object
|
|
* @type String
|
|
* @default
|
|
*/
|
|
type: 'i-text',
|
|
|
|
/**
|
|
* Index where text selection starts (or where cursor is when there is no selection)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
selectionStart: 0,
|
|
|
|
/**
|
|
* Index where text selection ends
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
selectionEnd: 0,
|
|
|
|
/**
|
|
* Color of text selection
|
|
* @type String
|
|
* @default
|
|
*/
|
|
selectionColor: 'rgba(17,119,255,0.3)',
|
|
|
|
/**
|
|
* Indicates whether text is in editing mode
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
isEditing: false,
|
|
|
|
/**
|
|
* Indicates whether a text can be edited
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
editable: true,
|
|
|
|
/**
|
|
* Border color of text object while it's in editing mode
|
|
* @type String
|
|
* @default
|
|
*/
|
|
editingBorderColor: 'rgba(102,153,255,0.25)',
|
|
|
|
/**
|
|
* Width of cursor (in px)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
cursorWidth: 2,
|
|
|
|
/**
|
|
* Color of default cursor (when not overwritten by character style)
|
|
* @type String
|
|
* @default
|
|
*/
|
|
cursorColor: '#333',
|
|
|
|
/**
|
|
* Delay between cursor blink (in ms)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
cursorDelay: 1000,
|
|
|
|
/**
|
|
* Duration of cursor fadein (in ms)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
cursorDuration: 600,
|
|
|
|
/**
|
|
* Indicates whether internal text char widths can be cached
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
caching: true,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_reSpace: /\s|\n/,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_currentCursorOpacity: 0,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_selectionDirection: null,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_abortCursorAnimation: false,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
__widthOfSpace: [],
|
|
|
|
/**
|
|
* Helps determining when the text is in composition, so that the cursor
|
|
* rendering is altered.
|
|
*/
|
|
inCompositionMode: false,
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {String} text Text string
|
|
* @param {Object} [options] Options object
|
|
* @return {fabric.IText} thisArg
|
|
*/
|
|
initialize: function(text, options) {
|
|
this.callSuper('initialize', text, options);
|
|
this.initBehavior();
|
|
},
|
|
|
|
/**
|
|
* Sets selection start (left boundary of a selection)
|
|
* @param {Number} index Index to set selection start to
|
|
*/
|
|
setSelectionStart: function(index) {
|
|
index = Math.max(index, 0);
|
|
this._updateAndFire('selectionStart', index);
|
|
},
|
|
|
|
/**
|
|
* Sets selection end (right boundary of a selection)
|
|
* @param {Number} index Index to set selection end to
|
|
*/
|
|
setSelectionEnd: function(index) {
|
|
index = Math.min(index, this.text.length);
|
|
this._updateAndFire('selectionEnd', index);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {String} property 'selectionStart' or 'selectionEnd'
|
|
* @param {Number} index new position of property
|
|
*/
|
|
_updateAndFire: function(property, index) {
|
|
if (this[property] !== index) {
|
|
this._fireSelectionChanged();
|
|
this[property] = index;
|
|
}
|
|
this._updateTextarea();
|
|
},
|
|
|
|
/**
|
|
* Fires the even of selection changed
|
|
* @private
|
|
*/
|
|
_fireSelectionChanged: function() {
|
|
this.fire('selection:changed');
|
|
this.canvas && this.canvas.fire('text:selection:changed', { target: this });
|
|
},
|
|
|
|
/**
|
|
* Initialize text dimensions. Render all text on given context
|
|
* or on a offscreen canvas to get the text width with measureText.
|
|
* Updates this.width and this.height with the proper values.
|
|
* Does not return dimensions.
|
|
* @private
|
|
*/
|
|
initDimensions: function() {
|
|
this.isEditing && this.initDelayedCursor();
|
|
this.clearContextTop();
|
|
this.callSuper('initDimensions');
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
render: function(ctx) {
|
|
this.clearContextTop();
|
|
this.callSuper('render', ctx);
|
|
// clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
|
|
// the correct position but not at every cursor animation.
|
|
this.cursorOffsetCache = { };
|
|
this.renderCursorOrSelection();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_render: function(ctx) {
|
|
this.callSuper('_render', ctx);
|
|
},
|
|
|
|
/**
|
|
* Prepare and clean the contextTop
|
|
*/
|
|
clearContextTop: function(skipRestore) {
|
|
if (!this.isEditing) {
|
|
return;
|
|
}
|
|
if (this.canvas && this.canvas.contextTop) {
|
|
var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform;
|
|
ctx.save();
|
|
ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
|
|
this.transform(ctx);
|
|
this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);
|
|
this._clearTextArea(ctx);
|
|
skipRestore || ctx.restore();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Renders cursor or selection (depending on what exists)
|
|
*/
|
|
renderCursorOrSelection: function() {
|
|
if (!this.isEditing || !this.canvas) {
|
|
return;
|
|
}
|
|
var boundaries = this._getCursorBoundaries(), ctx;
|
|
if (this.canvas && this.canvas.contextTop) {
|
|
ctx = this.canvas.contextTop;
|
|
this.clearContextTop(true);
|
|
}
|
|
else {
|
|
ctx = this.canvas.contextContainer;
|
|
ctx.save();
|
|
}
|
|
if (this.selectionStart === this.selectionEnd) {
|
|
this.renderCursor(boundaries, ctx);
|
|
}
|
|
else {
|
|
this.renderSelection(boundaries, ctx);
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
_clearTextArea: function(ctx) {
|
|
// we add 4 pixel, to be sure to do not leave any pixel out
|
|
var width = this.width + 4, height = this.height + 4;
|
|
ctx.clearRect(-width / 2, -height / 2, width, height);
|
|
},
|
|
|
|
/**
|
|
* Returns cursor boundaries (left, top, leftOffset, topOffset)
|
|
* @private
|
|
* @param {Array} chars Array of characters
|
|
* @param {String} typeOfBoundaries
|
|
*/
|
|
_getCursorBoundaries: function(position) {
|
|
|
|
// left/top are left/top of entire text box
|
|
// leftOffset/topOffset are offset from that left/top point of a text box
|
|
|
|
if (typeof position === 'undefined') {
|
|
position = this.selectionStart;
|
|
}
|
|
|
|
var left = this._getLeftOffset(),
|
|
top = this._getTopOffset(),
|
|
offsets = this._getCursorBoundariesOffsets(position);
|
|
|
|
return {
|
|
left: left,
|
|
top: top,
|
|
leftOffset: offsets.left,
|
|
topOffset: offsets.top
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getCursorBoundariesOffsets: function(position) {
|
|
if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
|
|
return this.cursorOffsetCache;
|
|
}
|
|
var lineLeftOffset,
|
|
lineIndex = 0,
|
|
charIndex = 0,
|
|
topOffset = 0,
|
|
leftOffset = 0,
|
|
boundaries,
|
|
cursorPosition = this.get2DCursorLocation(position);
|
|
for (var i = 0; i < cursorPosition.lineIndex; i++) {
|
|
topOffset += this.getHeightOfLine(i);
|
|
}
|
|
|
|
lineLeftOffset = this._getLineLeftOffset(cursorPosition.lineIndex);
|
|
var bound = this.__charBounds[cursorPosition.lineIndex][cursorPosition.charIndex];
|
|
bound && (leftOffset = bound.left);
|
|
if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
|
|
leftOffset -= this._getWidthOfCharSpacing();
|
|
}
|
|
boundaries = {
|
|
top: topOffset,
|
|
left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0),
|
|
};
|
|
this.cursorOffsetCache = boundaries;
|
|
return this.cursorOffsetCache;
|
|
},
|
|
|
|
/**
|
|
* Renders cursor
|
|
* @param {Object} boundaries
|
|
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
|
|
*/
|
|
renderCursor: function(boundaries, ctx) {
|
|
var cursorLocation = this.get2DCursorLocation(),
|
|
lineIndex = cursorLocation.lineIndex,
|
|
charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0,
|
|
charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'),
|
|
multiplier = this.scaleX * this.canvas.getZoom(),
|
|
cursorWidth = this.cursorWidth / multiplier,
|
|
topOffset = boundaries.topOffset;
|
|
|
|
topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight
|
|
- charHeight * (1 - this._fontSizeFraction);
|
|
|
|
if (this.inCompositionMode) {
|
|
this.renderSelection(boundaries, ctx);
|
|
}
|
|
|
|
ctx.fillStyle = this.getValueOfPropertyAt(lineIndex, charIndex, 'fill');
|
|
ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
|
|
ctx.fillRect(
|
|
boundaries.left + boundaries.leftOffset - cursorWidth / 2,
|
|
topOffset + boundaries.top,
|
|
cursorWidth,
|
|
charHeight);
|
|
},
|
|
|
|
/**
|
|
* Renders text selection
|
|
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
|
|
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
|
|
*/
|
|
renderSelection: function(boundaries, ctx) {
|
|
|
|
var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
|
|
selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd,
|
|
isJustify = this.textAlign.indexOf('justify') !== -1,
|
|
start = this.get2DCursorLocation(selectionStart),
|
|
end = this.get2DCursorLocation(selectionEnd),
|
|
startLine = start.lineIndex,
|
|
endLine = end.lineIndex,
|
|
startChar = start.charIndex < 0 ? 0 : start.charIndex,
|
|
endChar = end.charIndex < 0 ? 0 : end.charIndex;
|
|
|
|
for (var i = startLine; i <= endLine; i++) {
|
|
var lineOffset = this._getLineLeftOffset(i) || 0,
|
|
lineHeight = this.getHeightOfLine(i),
|
|
realLineHeight = 0, boxStart = 0, boxEnd = 0;
|
|
|
|
if (i === startLine) {
|
|
boxStart = this.__charBounds[startLine][startChar].left;
|
|
}
|
|
if (i >= startLine && i < endLine) {
|
|
boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5?
|
|
}
|
|
else if (i === endLine) {
|
|
if (endChar === 0) {
|
|
boxEnd = this.__charBounds[endLine][endChar].left;
|
|
}
|
|
else {
|
|
boxEnd = this.__charBounds[endLine][endChar - 1].left + this.__charBounds[endLine][endChar - 1].width;
|
|
}
|
|
}
|
|
realLineHeight = lineHeight;
|
|
if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {
|
|
lineHeight /= this.lineHeight;
|
|
}
|
|
if (this.inCompositionMode) {
|
|
ctx.fillStyle = this.compositionColor || 'black';
|
|
ctx.fillRect(
|
|
boundaries.left + lineOffset + boxStart,
|
|
boundaries.top + boundaries.topOffset + lineHeight,
|
|
boxEnd - boxStart,
|
|
1);
|
|
}
|
|
else {
|
|
ctx.fillStyle = this.selectionColor;
|
|
ctx.fillRect(
|
|
boundaries.left + lineOffset + boxStart,
|
|
boundaries.top + boundaries.topOffset,
|
|
boxEnd - boxStart,
|
|
lineHeight);
|
|
}
|
|
|
|
|
|
boundaries.topOffset += realLineHeight;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* High level function to know the height of the cursor.
|
|
* the currentChar is the one that precedes the cursor
|
|
* Returns fontSize of char at the current cursor
|
|
* @return {Number} Character font size
|
|
*/
|
|
getCurrentCharFontSize: function() {
|
|
var cp = this._getCurrentCharIndex();
|
|
return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize');
|
|
},
|
|
|
|
/**
|
|
* High level function to know the color of the cursor.
|
|
* the currentChar is the one that precedes the cursor
|
|
* Returns color (fill) of char at the current cursor
|
|
* @return {String} Character color (fill)
|
|
*/
|
|
getCurrentCharColor: function() {
|
|
var cp = this._getCurrentCharIndex();
|
|
return this.getValueOfPropertyAt(cp.l, cp.c, 'fill');
|
|
},
|
|
|
|
/**
|
|
* Returns the cursor position for the getCurrent.. functions
|
|
* @private
|
|
*/
|
|
_getCurrentCharIndex: function() {
|
|
var cursorPosition = this.get2DCursorLocation(this.selectionStart, true),
|
|
charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0;
|
|
return { l: cursorPosition.lineIndex, c: charIndex };
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returns fabric.IText instance from an object representation
|
|
* @static
|
|
* @memberOf fabric.IText
|
|
* @param {Object} object Object to create an instance from
|
|
* @param {function} [callback] invoked with new instance as argument
|
|
*/
|
|
fabric.IText.fromObject = function(object, callback) {
|
|
parseDecoration(object);
|
|
if (object.styles) {
|
|
for (var i in object.styles) {
|
|
for (var j in object.styles[i]) {
|
|
parseDecoration(object.styles[i][j]);
|
|
}
|
|
}
|
|
}
|
|
fabric.Object._fromObject('IText', object, callback, 'text');
|
|
};
|
|
})();
|