From a1e578b137a12fd79f1a9d2b07912d8e6cf1b352 Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 27 May 2015 11:44:53 -0700 Subject: [PATCH 1/6] Textbox implementaion from Jaffer Haider, with one minor change in textbox_key_behavior --- build.js | 5 +++ build.sh | 0 build_all | 0 src/mixins/texbox_key_behavior.mixin.js | 22 ++++++++++++ src/mixins/textbox_behavior.mixin.js | 41 ++++++++++++++++++++++ src/mixins/textbox_click_behavior.mixin.js | 22 ++++++++++++ src/shapes/itext.class.js | 8 ++--- src/shapes/text.class.js | 17 ++++++++- 8 files changed, 110 insertions(+), 5 deletions(-) mode change 100755 => 100644 build.sh mode change 100755 => 100644 build_all create mode 100644 src/mixins/texbox_key_behavior.mixin.js create mode 100644 src/mixins/textbox_behavior.mixin.js create mode 100644 src/mixins/textbox_click_behavior.mixin.js diff --git a/build.js b/build.js index fb6f0374..27f83faa 100644 --- a/build.js +++ b/build.js @@ -244,6 +244,11 @@ var filesToInclude = [ ifSpecifiedInclude('itext', 'src/mixins/itext_key_behavior.mixin.js'), ifSpecifiedInclude('itext', 'src/mixins/itext.svg_export.js'), + ifSpecifiedInclude('textbox', 'src/shapes/textbox.class.js'), + ifSpecifiedInclude('textbox', 'src/mixins/textbox_behavior.mixin.js'), + ifSpecifiedInclude('textbox', 'src/mixins/textbox_click_behavior.mixin.js'), + ifSpecifiedInclude('textbox', 'src/mixins/textbox_key_behavior.mixin.js'), + ifSpecifiedInclude('node', 'src/node.js'), ifSpecifiedAMDInclude(amdLib) diff --git a/build.sh b/build.sh old mode 100755 new mode 100644 diff --git a/build_all b/build_all old mode 100755 new mode 100644 diff --git a/src/mixins/texbox_key_behavior.mixin.js b/src/mixins/texbox_key_behavior.mixin.js new file mode 100644 index 00000000..7f9a095f --- /dev/null +++ b/src/mixins/texbox_key_behavior.mixin.js @@ -0,0 +1,22 @@ +fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.Textbox.prototype */ { + /** + * Overrides superclass function and adjusts cursor offset value because + * lines do not necessarily end with a newline in Textbox. + * @param {Event} e + * @param {Boolean} isRight + * @returns {Number} + */ + getDownCursorOffset: function(e, isRight) { + return fabric.IText.prototype.getDownCursorOffset.apply(this, [e, isRight]) - 1; + }, + /** + * Overrides superclass function and adjusts cursor offset value because + * lines do not necessarily end with a newline in Textbox. + * @param {Event} e + * @param {Boolean} isRight + * @returns {Number} + */ + getUpCursorOffset: function(e, isRight) { + return fabric.IText.prototype.getUpCursorOffset.apply(this, [e, isRight]) - 1; + } +}); \ No newline at end of file diff --git a/src/mixins/textbox_behavior.mixin.js b/src/mixins/textbox_behavior.mixin.js new file mode 100644 index 00000000..dd427949 --- /dev/null +++ b/src/mixins/textbox_behavior.mixin.js @@ -0,0 +1,41 @@ +(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) { + + var t = transform.target; + if (t instanceof fabric.Textbox) { + var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth)); + if (w >= t.minWidth) { + t.set('width', w); + } + } + else { + setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip); + } + }; + + /** + * 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; + } + } + }; + +})(); \ No newline at end of file diff --git a/src/mixins/textbox_click_behavior.mixin.js b/src/mixins/textbox_click_behavior.mixin.js new file mode 100644 index 00000000..36f06fdd --- /dev/null +++ b/src/mixins/textbox_click_behavior.mixin.js @@ -0,0 +1,22 @@ +(function() { + var getNewSelectionStartFromOffsetOverriden = fabric.IText.prototype._getNewSelectionStartFromOffset; + /** + * Overrides the IText implementation and always sends lineIndex as 0 for Textboxes. + * @param {Number} mouseOffset + * @param {Number} prevWidth + * @param {Number} width + * @param {Number} index + * @param {Number} lineIndex + * @param {Number} jlen + * @returns {Number} + */ + fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, + prevWidth, width, index, lineIndex, jlen) { + if (this instanceof fabric.Textbox) { + lineIndex = 0; + } + return getNewSelectionStartFromOffsetOverriden + .call(this, mouseOffset, + prevWidth, width, index, lineIndex, jlen); + }; +})(); \ No newline at end of file diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 277b37df..b3f2912b 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -59,14 +59,14 @@ /** * Index where text selection starts (or where cursor is when there is no selection) - * @type Nubmer + * @type Number * @default */ selectionStart: 0, /** * Index where text selection ends - * @type Nubmer + * @type Number * @default */ selectionEnd: 0, @@ -932,7 +932,7 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { - if (this.textAlign === 'justify' && /\s/.test(_char)) { + if (this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) { return this._getWidthOfSpace(ctx, lineIndex); } @@ -1008,7 +1008,7 @@ var line = this._textLines[lineIndex], wordsWidth = this._getWidthOfWords(ctx, line, lineIndex), widthDiff = this.width - wordsWidth, - numSpaces = line.length - line.replace(/\s+/g, '').length, + numSpaces = line.length - line.replace(this._reSpacesAndTabs, '').length, width = widthDiff / numSpaces; this.__widthOfSpace[lineIndex] = width; return width; diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 66a9164f..81cc214f 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -59,6 +59,13 @@ */ _reNewline: /\r?\n/, + /** + * Use this regular expression to filter for whitespace that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpacesAndTabs: /[ \t\r]+/g, + /** * Retrieves object's fontSize * @method getFontSize @@ -329,7 +336,7 @@ ctx = fabric.util.createCanvasElement().getContext('2d'); this._setTextStyles(ctx); } - this._textLines = this.text.split(this._reNewline); + this._textLines = this._splitTextIntoLines(); this._clearCache(); var currentTextAlign = this.textAlign; this.textAlign = 'left'; @@ -798,6 +805,14 @@ ctx.restore(); }, + /** + * 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 From 1062fdb26eac73c8ca6e56a9098e35fb1d8603a4 Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 27 May 2015 11:47:13 -0700 Subject: [PATCH 2/6] missed main textbox class, and autoformatted changes. --- src/mixins/texbox_key_behavior.mixin.js | 4 +- src/mixins/textbox_behavior.mixin.js | 10 +- src/mixins/textbox_click_behavior.mixin.js | 6 +- src/shapes/textbox.class.js | 317 +++++++++++++++++++++ 4 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 src/shapes/textbox.class.js diff --git a/src/mixins/texbox_key_behavior.mixin.js b/src/mixins/texbox_key_behavior.mixin.js index 7f9a095f..64089c20 100644 --- a/src/mixins/texbox_key_behavior.mixin.js +++ b/src/mixins/texbox_key_behavior.mixin.js @@ -6,7 +6,7 @@ fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.Textbox.pr * @param {Boolean} isRight * @returns {Number} */ - getDownCursorOffset: function(e, isRight) { + getDownCursorOffset: function (e, isRight) { return fabric.IText.prototype.getDownCursorOffset.apply(this, [e, isRight]) - 1; }, /** @@ -16,7 +16,7 @@ fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.Textbox.pr * @param {Boolean} isRight * @returns {Number} */ - getUpCursorOffset: function(e, isRight) { + getUpCursorOffset: function (e, isRight) { return fabric.IText.prototype.getUpCursorOffset.apply(this, [e, isRight]) - 1; } }); \ No newline at end of file diff --git a/src/mixins/textbox_behavior.mixin.js b/src/mixins/textbox_behavior.mixin.js index dd427949..d9edc850 100644 --- a/src/mixins/textbox_behavior.mixin.js +++ b/src/mixins/textbox_behavior.mixin.js @@ -1,12 +1,12 @@ -(function() { +(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) { + fabric.Canvas.prototype._setObjectScale = function (localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip) { var t = transform.target; if (t instanceof fabric.Textbox) { @@ -26,11 +26,11 @@ * one is present in the group. Deletes _controlsVisibility otherwise, so that * it gets initialized to default value at runtime. */ - fabric.Group.prototype._refreshControlsVisibility = function() { + fabric.Group.prototype._refreshControlsVisibility = function () { if (typeof fabric.Textbox === 'undefined') { return; } - for (var i = this._objects.length; i--; ) { + for (var i = this._objects.length; i--;) { if (this._objects[i] instanceof fabric.Textbox) { this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); return; diff --git a/src/mixins/textbox_click_behavior.mixin.js b/src/mixins/textbox_click_behavior.mixin.js index 36f06fdd..5aa7ba7d 100644 --- a/src/mixins/textbox_click_behavior.mixin.js +++ b/src/mixins/textbox_click_behavior.mixin.js @@ -1,4 +1,4 @@ -(function() { +(function () { var getNewSelectionStartFromOffsetOverriden = fabric.IText.prototype._getNewSelectionStartFromOffset; /** * Overrides the IText implementation and always sends lineIndex as 0 for Textboxes. @@ -10,8 +10,8 @@ * @param {Number} jlen * @returns {Number} */ - fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, - prevWidth, width, index, lineIndex, jlen) { + fabric.IText.prototype._getNewSelectionStartFromOffset = function (mouseOffset, + prevWidth, width, index, lineIndex, jlen) { if (this instanceof fabric.Textbox) { lineIndex = 0; } diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js new file mode 100644 index 00000000..01ac7d88 --- /dev/null +++ b/src/shapes/textbox.class.js @@ -0,0 +1,317 @@ +(function () { + var clone = fabric.util.object.clone; + + /** + * 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, + /** + * Cached array of text wrapping. + * @type Array + */ + __cachedLines: null, + /** + * 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.ctx = fabric.util.createCanvasElement().getContext('2d'); + + this.callSuper('initialize', text, options); + this.set({ + lockUniScaling: false, + lockScalingY: true, + lockScalingFlip: true, + hasBorders: true + }); + this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); + + // add width to this list of props that effect line wrapping. + this._dimensionAffectingProps.width = true; + }, + /** + * 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._textLines = this._splitTextIntoLines(); + this._clearCache(); + this.height = this._getTextHeight(ctx); + }, + /** + * 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])); + } + + return wrapped; + }, + /** + * 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 + * @returns {Array} Array of line(s) into which the given text is wrapped + * to. + */ + _wrapLine: function (ctx, text) { + var maxWidth = this.width, words = text.split(' '), + lines = [], + line = ''; + + if (ctx.measureText(text).width < maxWidth) { + lines.push(text); + } + else { + while (words.length > 0) { + + /* + * If the textbox's width is less than the widest letter. + * TODO: Performance improvement - cache the width of W whenever + * fontSize changes. + */ + if (maxWidth <= ctx.measureText('W').width) { + return text.split(''); + } + + /* + * This handles a word that is longer than the width of the + * text area. + */ + while (Math.ceil(ctx.measureText(words[0]).width) >= maxWidth) { + var tmp = words[0]; + words[0] = tmp.slice(0, -1); + if (words.length > 1) { + words[1] = tmp.slice(-1) + words[1]; + } + else { + words.push(tmp.slice(-1)); + } + } + + if (Math.ceil(ctx.measureText(line + words[0]).width) < maxWidth) { + line += words.shift() + ' '; + } + else { + lines.push(line); + line = ''; + } + if (words.length === 0) { + lines.push(line.substring(0, line.length - 1)); + } + } + } + + 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 () { + this.ctx.save(); + this._setTextStyles(this.ctx); + + lines = this._wrapText(this.ctx, this.text); + + this.ctx.restore(); + 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 {Any} value + */ + setOnGroup: function (key, value) { + if (key === 'scaleX') { + this.set(key, 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. + * @returns {Object} This object has 'lineIndex' and 'charIndex' properties set to Numbers. + */ + get2DCursorLocation: function (selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + + /* + * We use `temp` to populate linesBeforeCursor instead of simply splitting + * textBeforeCursor with newlines to handle the case of the + * selectionStart value being on a word that, because of its length, + * needs to be wrapped to the next line. + */ + var lineIndex = 0, + linesBeforeCursor = [], + allLines = this._textLines, temp = selectionStart; + + while (temp >= 0) { + if (lineIndex > allLines.length - 1) { + break; + } + temp -= allLines[lineIndex].length; + if (temp < 0) { + linesBeforeCursor[linesBeforeCursor.length] = allLines[lineIndex].slice(0, + temp + allLines[lineIndex].length); + } + else { + linesBeforeCursor[linesBeforeCursor.length] = allLines[lineIndex]; + } + lineIndex++; + } + lineIndex--; + + var lastLine = linesBeforeCursor[linesBeforeCursor.length - 1], + charIndex = lastLine.length; + + if (linesBeforeCursor[lineIndex] === allLines[lineIndex]) { + if (lineIndex + 1 < allLines.length - 1) { + lineIndex++; + charIndex = 0; + } + } + + return { + lineIndex: lineIndex, + charIndex: charIndex + }; + }, + /** + * 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._getCachedLineOffset(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 + }; + }, + /** + * 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), { + minWidth: this.minWidth + }); + } + }); + /** + * Returns fabric.Textbox instance from an object representation + * @static + * @memberOf fabric.Textbox + * @param {Object} object Object to create an instance from + * @return {fabric.Textbox} instance of fabric.Textbox + */ + fabric.Textbox.fromObject = function (object) { + return new fabric.Textbox(object.text, clone(object)); + }; + /** + * 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 + }; + }; + /** + * Contains all fabric.Textbox objects that have been created + * @static + * @memberof fabric.Textbox + * @type Array + */ + fabric.Textbox.instances = []; +})(); \ No newline at end of file From 49a893d06fe8ed14c5a4f2bf4922dfbbe6267163 Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 27 May 2015 11:47:45 -0700 Subject: [PATCH 3/6] typo.. --- ...texbox_key_behavior.mixin.js => textbox_key_behavior.mixin.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/mixins/{texbox_key_behavior.mixin.js => textbox_key_behavior.mixin.js} (100%) diff --git a/src/mixins/texbox_key_behavior.mixin.js b/src/mixins/textbox_key_behavior.mixin.js similarity index 100% rename from src/mixins/texbox_key_behavior.mixin.js rename to src/mixins/textbox_key_behavior.mixin.js From 8c7ce62b81569d0b3432ecde5ebe386c9e147952 Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 27 May 2015 13:30:35 -0700 Subject: [PATCH 4/6] revert filemode changes --- build.sh | 0 build_all | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 build.sh mode change 100644 => 100755 build_all diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/build_all b/build_all old mode 100644 new mode 100755 From 1067ac028aee163dd3bd5937fcda5e6ed1a48db7 Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 27 May 2015 13:59:56 -0700 Subject: [PATCH 5/6] forgot to set proper style guide when auto-formatting --- src/mixins/textbox_behavior.mixin.js | 72 +-- src/mixins/textbox_click_behavior.mixin.js | 42 +- src/mixins/textbox_key_behavior.mixin.js | 42 +- src/shapes/textbox.class.js | 616 ++++++++++----------- 4 files changed, 386 insertions(+), 386 deletions(-) diff --git a/src/mixins/textbox_behavior.mixin.js b/src/mixins/textbox_behavior.mixin.js index d9edc850..966b870e 100644 --- a/src/mixins/textbox_behavior.mixin.js +++ b/src/mixins/textbox_behavior.mixin.js @@ -1,41 +1,41 @@ (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) { + /** + * 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) { - var t = transform.target; - if (t instanceof fabric.Textbox) { - var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth)); - if (w >= t.minWidth) { - t.set('width', w); - } - } - else { - setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, - lockScalingX, lockScalingY, by, lockScalingFlip); - } - }; + var t = transform.target; + if (t instanceof fabric.Textbox) { + var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth)); + if (w >= t.minWidth) { + t.set('width', w); + } + } + else { + setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip); + } + }; - /** - * 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; - } - } - }; + /** + * 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; + } + } + }; -})(); \ No newline at end of file +})(); diff --git a/src/mixins/textbox_click_behavior.mixin.js b/src/mixins/textbox_click_behavior.mixin.js index 5aa7ba7d..c0ef1c0e 100644 --- a/src/mixins/textbox_click_behavior.mixin.js +++ b/src/mixins/textbox_click_behavior.mixin.js @@ -1,22 +1,22 @@ (function () { - var getNewSelectionStartFromOffsetOverriden = fabric.IText.prototype._getNewSelectionStartFromOffset; - /** - * Overrides the IText implementation and always sends lineIndex as 0 for Textboxes. - * @param {Number} mouseOffset - * @param {Number} prevWidth - * @param {Number} width - * @param {Number} index - * @param {Number} lineIndex - * @param {Number} jlen - * @returns {Number} - */ - fabric.IText.prototype._getNewSelectionStartFromOffset = function (mouseOffset, - prevWidth, width, index, lineIndex, jlen) { - if (this instanceof fabric.Textbox) { - lineIndex = 0; - } - return getNewSelectionStartFromOffsetOverriden - .call(this, mouseOffset, - prevWidth, width, index, lineIndex, jlen); - }; -})(); \ No newline at end of file + var getNewSelectionStartFromOffsetOverriden = fabric.IText.prototype._getNewSelectionStartFromOffset; + /** + * Overrides the IText implementation and always sends lineIndex as 0 for Textboxes. + * @param {Number} mouseOffset + * @param {Number} prevWidth + * @param {Number} width + * @param {Number} index + * @param {Number} lineIndex + * @param {Number} jlen + * @returns {Number} + */ + fabric.IText.prototype._getNewSelectionStartFromOffset = function (mouseOffset, + prevWidth, width, index, lineIndex, jlen) { + if (this instanceof fabric.Textbox) { + lineIndex = 0; + } + return getNewSelectionStartFromOffsetOverriden + .call(this, mouseOffset, + prevWidth, width, index, lineIndex, jlen); + }; +})(); diff --git a/src/mixins/textbox_key_behavior.mixin.js b/src/mixins/textbox_key_behavior.mixin.js index 64089c20..c1170d41 100644 --- a/src/mixins/textbox_key_behavior.mixin.js +++ b/src/mixins/textbox_key_behavior.mixin.js @@ -1,22 +1,22 @@ fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.Textbox.prototype */ { - /** - * Overrides superclass function and adjusts cursor offset value because - * lines do not necessarily end with a newline in Textbox. - * @param {Event} e - * @param {Boolean} isRight - * @returns {Number} - */ - getDownCursorOffset: function (e, isRight) { - return fabric.IText.prototype.getDownCursorOffset.apply(this, [e, isRight]) - 1; - }, - /** - * Overrides superclass function and adjusts cursor offset value because - * lines do not necessarily end with a newline in Textbox. - * @param {Event} e - * @param {Boolean} isRight - * @returns {Number} - */ - getUpCursorOffset: function (e, isRight) { - return fabric.IText.prototype.getUpCursorOffset.apply(this, [e, isRight]) - 1; - } -}); \ No newline at end of file + /** + * Overrides superclass function and adjusts cursor offset value because + * lines do not necessarily end with a newline in Textbox. + * @param {Event} e + * @param {Boolean} isRight + * @returns {Number} + */ + getDownCursorOffset: function (e, isRight) { + return fabric.IText.prototype.getDownCursorOffset.apply(this, [e, isRight]) - 1; + }, + /** + * Overrides superclass function and adjusts cursor offset value because + * lines do not necessarily end with a newline in Textbox. + * @param {Event} e + * @param {Boolean} isRight + * @returns {Number} + */ + getUpCursorOffset: function (e, isRight) { + return fabric.IText.prototype.getUpCursorOffset.apply(this, [e, isRight]) - 1; + } +}); diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index 01ac7d88..cb7fd463 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -1,317 +1,317 @@ (function () { - var clone = fabric.util.object.clone; + var clone = fabric.util.object.clone; + /** + * 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, { /** - * 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 + * Type of an object + * @type String + * @default */ - 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, - /** - * Cached array of text wrapping. - * @type Array - */ - __cachedLines: null, - /** - * 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.ctx = fabric.util.createCanvasElement().getContext('2d'); - - this.callSuper('initialize', text, options); - this.set({ - lockUniScaling: false, - lockScalingY: true, - lockScalingFlip: true, - hasBorders: true - }); - this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); - - // add width to this list of props that effect line wrapping. - this._dimensionAffectingProps.width = true; - }, - /** - * 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._textLines = this._splitTextIntoLines(); - this._clearCache(); - this.height = this._getTextHeight(ctx); - }, - /** - * 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])); - } - - return wrapped; - }, - /** - * 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 - * @returns {Array} Array of line(s) into which the given text is wrapped - * to. - */ - _wrapLine: function (ctx, text) { - var maxWidth = this.width, words = text.split(' '), - lines = [], - line = ''; - - if (ctx.measureText(text).width < maxWidth) { - lines.push(text); - } - else { - while (words.length > 0) { - - /* - * If the textbox's width is less than the widest letter. - * TODO: Performance improvement - cache the width of W whenever - * fontSize changes. - */ - if (maxWidth <= ctx.measureText('W').width) { - return text.split(''); - } - - /* - * This handles a word that is longer than the width of the - * text area. - */ - while (Math.ceil(ctx.measureText(words[0]).width) >= maxWidth) { - var tmp = words[0]; - words[0] = tmp.slice(0, -1); - if (words.length > 1) { - words[1] = tmp.slice(-1) + words[1]; - } - else { - words.push(tmp.slice(-1)); - } - } - - if (Math.ceil(ctx.measureText(line + words[0]).width) < maxWidth) { - line += words.shift() + ' '; - } - else { - lines.push(line); - line = ''; - } - if (words.length === 0) { - lines.push(line.substring(0, line.length - 1)); - } - } - } - - 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 () { - this.ctx.save(); - this._setTextStyles(this.ctx); - - lines = this._wrapText(this.ctx, this.text); - - this.ctx.restore(); - 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 {Any} value - */ - setOnGroup: function (key, value) { - if (key === 'scaleX') { - this.set(key, 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. - * @returns {Object} This object has 'lineIndex' and 'charIndex' properties set to Numbers. - */ - get2DCursorLocation: function (selectionStart) { - if (typeof selectionStart === 'undefined') { - selectionStart = this.selectionStart; - } - - /* - * We use `temp` to populate linesBeforeCursor instead of simply splitting - * textBeforeCursor with newlines to handle the case of the - * selectionStart value being on a word that, because of its length, - * needs to be wrapped to the next line. - */ - var lineIndex = 0, - linesBeforeCursor = [], - allLines = this._textLines, temp = selectionStart; - - while (temp >= 0) { - if (lineIndex > allLines.length - 1) { - break; - } - temp -= allLines[lineIndex].length; - if (temp < 0) { - linesBeforeCursor[linesBeforeCursor.length] = allLines[lineIndex].slice(0, - temp + allLines[lineIndex].length); - } - else { - linesBeforeCursor[linesBeforeCursor.length] = allLines[lineIndex]; - } - lineIndex++; - } - lineIndex--; - - var lastLine = linesBeforeCursor[linesBeforeCursor.length - 1], - charIndex = lastLine.length; - - if (linesBeforeCursor[lineIndex] === allLines[lineIndex]) { - if (lineIndex + 1 < allLines.length - 1) { - lineIndex++; - charIndex = 0; - } - } - - return { - lineIndex: lineIndex, - charIndex: charIndex - }; - }, - /** - * 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._getCachedLineOffset(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 - }; - }, - /** - * 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), { - minWidth: this.minWidth - }); - } - }); + type: 'textbox', /** - * Returns fabric.Textbox instance from an object representation - * @static - * @memberOf fabric.Textbox - * @param {Object} object Object to create an instance from - * @return {fabric.Textbox} instance of fabric.Textbox + * Minimum width of textbox, in pixels. + * @type Number + * @default */ - fabric.Textbox.fromObject = function (object) { - return new fabric.Textbox(object.text, clone(object)); - }; + minWidth: 20, /** - * 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 - }; - }; - /** - * Contains all fabric.Textbox objects that have been created - * @static - * @memberof fabric.Textbox + * Cached array of text wrapping. * @type Array */ - fabric.Textbox.instances = []; -})(); \ No newline at end of file + __cachedLines: null, + /** + * 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.ctx = fabric.util.createCanvasElement().getContext('2d'); + + this.callSuper('initialize', text, options); + this.set({ + lockUniScaling: false, + lockScalingY: true, + lockScalingFlip: true, + hasBorders: true + }); + this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); + + // add width to this list of props that effect line wrapping. + this._dimensionAffectingProps.width = true; + }, + /** + * 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._textLines = this._splitTextIntoLines(); + this._clearCache(); + this.height = this._getTextHeight(ctx); + }, + /** + * 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])); + } + + return wrapped; + }, + /** + * 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 + * @returns {Array} Array of line(s) into which the given text is wrapped + * to. + */ + _wrapLine: function (ctx, text) { + var maxWidth = this.width, words = text.split(' '), + lines = [], + line = ''; + + if (ctx.measureText(text).width < maxWidth) { + lines.push(text); + } + else { + while (words.length > 0) { + + /* + * If the textbox's width is less than the widest letter. + * TODO: Performance improvement - cache the width of W whenever + * fontSize changes. + */ + if (maxWidth <= ctx.measureText('W').width) { + return text.split(''); + } + + /* + * This handles a word that is longer than the width of the + * text area. + */ + while (Math.ceil(ctx.measureText(words[0]).width) >= maxWidth) { + var tmp = words[0]; + words[0] = tmp.slice(0, -1); + if (words.length > 1) { + words[1] = tmp.slice(-1) + words[1]; + } + else { + words.push(tmp.slice(-1)); + } + } + + if (Math.ceil(ctx.measureText(line + words[0]).width) < maxWidth) { + line += words.shift() + ' '; + } + else { + lines.push(line); + line = ''; + } + if (words.length === 0) { + lines.push(line.substring(0, line.length - 1)); + } + } + } + + 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 () { + this.ctx.save(); + this._setTextStyles(this.ctx); + + lines = this._wrapText(this.ctx, this.text); + + this.ctx.restore(); + 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 {Any} value + */ + setOnGroup: function (key, value) { + if (key === 'scaleX') { + this.set(key, 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. + * @returns {Object} This object has 'lineIndex' and 'charIndex' properties set to Numbers. + */ + get2DCursorLocation: function (selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + + /* + * We use `temp` to populate linesBeforeCursor instead of simply splitting + * textBeforeCursor with newlines to handle the case of the + * selectionStart value being on a word that, because of its length, + * needs to be wrapped to the next line. + */ + var lineIndex = 0, + linesBeforeCursor = [], + allLines = this._textLines, temp = selectionStart; + + while (temp >= 0) { + if (lineIndex > allLines.length - 1) { + break; + } + temp -= allLines[lineIndex].length; + if (temp < 0) { + linesBeforeCursor[linesBeforeCursor.length] = allLines[lineIndex].slice(0, + temp + allLines[lineIndex].length); + } + else { + linesBeforeCursor[linesBeforeCursor.length] = allLines[lineIndex]; + } + lineIndex++; + } + lineIndex--; + + var lastLine = linesBeforeCursor[linesBeforeCursor.length - 1], + charIndex = lastLine.length; + + if (linesBeforeCursor[lineIndex] === allLines[lineIndex]) { + if (lineIndex + 1 < allLines.length - 1) { + lineIndex++; + charIndex = 0; + } + } + + return { + lineIndex: lineIndex, + charIndex: charIndex + }; + }, + /** + * 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._getCachedLineOffset(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 + }; + }, + /** + * 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), { + minWidth: this.minWidth + }); + } + }); + /** + * Returns fabric.Textbox instance from an object representation + * @static + * @memberOf fabric.Textbox + * @param {Object} object Object to create an instance from + * @return {fabric.Textbox} instance of fabric.Textbox + */ + fabric.Textbox.fromObject = function (object) { + return new fabric.Textbox(object.text, clone(object)); + }; + /** + * 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 + }; + }; + /** + * Contains all fabric.Textbox objects that have been created + * @static + * @memberof fabric.Textbox + * @type Array + */ + fabric.Textbox.instances = []; +})(); From bb32d78db170a9356ab0bf0124ff846748de9647 Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 27 May 2015 14:18:52 -0700 Subject: [PATCH 6/6] adhere to 120 character limit --- src/shapes/textbox.class.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index cb7fd463..9304b8bc 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -258,8 +258,9 @@ } if (typeOfBoundaries === 'cursor') { - topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex) / this.lineHeight - - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex) * (1 - this._fontSizeFraction); + topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex) + / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex) + * (1 - this._fontSizeFraction); } return {