From 9eb597c7294dc2bd9ec29b5135eb22924561be9b Mon Sep 17 00:00:00 2001 From: inssein Date: Wed, 10 Jun 2015 17:12:54 -0700 Subject: [PATCH] Use this.styles as if it's not affected by line-wraps. --- src/mixins/itext_behavior.mixin.js | 4 +- src/mixins/textbox_behavior.mixin.js | 142 +++++++++++++++++++++++++++ src/shapes/itext.class.js | 118 +++++++++++++++------- src/shapes/textbox.class.js | 124 +++++++++++++++++++++-- 4 files changed, 338 insertions(+), 50 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 7f729b31..c443e8b1 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -628,8 +628,8 @@ lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex; - if (!this.styles[lineIndex]) { - this.styles[lineIndex] = { }; + if (!this._getLineStyle(lineIndex)) { + this._setLineStyle(lineIndex, {}); } if (_chars === '\n') { diff --git a/src/mixins/textbox_behavior.mixin.js b/src/mixins/textbox_behavior.mixin.js index 966b870e..bc87ed93 100644 --- a/src/mixins/textbox_behavior.mixin.js +++ b/src/mixins/textbox_behavior.mixin.js @@ -5,6 +5,7 @@ * 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) { @@ -38,4 +39,145 @@ } }; + var clone = fabric.util.object.clone; + + 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; + + this.callSuper('insertCharStyleObject', 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; + + this.callSuper('insertNewlineStyleObject', 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 clonedStyles = clone(this.styles); + + // adjust line index + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + + 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 + }, + + /** + * Figure out programatically the text on previous actual line (actual = separated by \n); + * + * @param {Number} lineIndex + * @returns {String} + * @private + */ + _getTextOnPreviousLine: function(lineIndex) { + var textOnPreviousLine = this._textLines[lineIndex - 1]; + + while(this._styleMap[lineIndex - 2] && this._styleMap[lineIndex - 2].line === this._styleMap[lineIndex - 1].line) { + textOnPreviousLine = this._textLines[lineIndex - 2] + textOnPreviousLine; + + lineIndex--; + } + + 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; + + 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]; + //console.log('deleting', lineIndex, charIndex + offset); + } + + 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]; + } + } + } + } + }); })(); diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index b3f2912b..205c91f7 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -260,11 +260,8 @@ } var loc = this.get2DCursorLocation(startIndex); - if (this.styles[loc.lineIndex]) { - return this.styles[loc.lineIndex][loc.charIndex] || { }; - } - - return { }; + var style = this._getStyleDeclaration(loc.lineIndex, loc.charIndex); + return style || {}; }, /** @@ -293,13 +290,15 @@ _extendStyles: function(index, styles) { var loc = this.get2DCursorLocation(index); - if (!this.styles[loc.lineIndex]) { - this.styles[loc.lineIndex] = { }; + if (!this._getLineStyle(loc.lineIndex)) { + this._setLineStyle(loc.lineIndex, {}) } - if (!this.styles[loc.lineIndex][loc.charIndex]) { - this.styles[loc.lineIndex][loc.charIndex] = { }; + + if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { + this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); } - fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles); + + fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); }, /** @@ -378,7 +377,7 @@ * @return {Object} Character style */ getCurrentCharStyle: function(lineIndex, charIndex) { - var style = this.styles[lineIndex] && this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)]; + var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); return { fontSize: style && style.fontSize || this.fontSize, @@ -400,10 +399,8 @@ * @return {Number} Character font size */ getCurrentCharFontSize: function(lineIndex, charIndex) { - return ( - this.styles[lineIndex] && - this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && - this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fontSize) || this.fontSize; + var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); + return style && style.fontSize ? style.fontSize : this.fontSize; }, /** @@ -413,10 +410,8 @@ * @return {String} Character color (fill) */ getCurrentCharColor: function(lineIndex, charIndex) { - return ( - this.styles[lineIndex] && - this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && - this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor; + var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); + return style && style.fill ? style.fill : this.cursorColor; }, /** @@ -644,11 +639,11 @@ * @param {Number} lineHeight Height of the line */ _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { - var decl, charWidth, charHeight, + var charWidth, charHeight, + decl = this._getStyleDeclaration(lineIndex, i), offset = this._fontSizeFraction * lineHeight / this.lineHeight; - if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { - + if (decl) { var shouldStroke = decl.stroke || this.stroke, shouldFill = decl.fill || this.fill; @@ -803,13 +798,14 @@ heightOfLine / this.lineHeight ); } - if (this.styles[i]) { + if (this._getLineStyle(i)) { for (var j = 0, jlen = this._textLines[i].length; j < jlen; j++) { - if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { + var style = this._getStyleDeclaration(i, j); + if (style && style.textBackgroundColor) { var _char = this._textLines[i][j]; - ctx.fillStyle = this.styles[i][j].textBackgroundColor; + ctx.fillStyle = style.textBackgroundColor; ctx.fillRect( this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j), @@ -846,9 +842,7 @@ * @param {Object} [decl] */ _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { - var styleDeclaration = decl || - (this.styles[lineIndex] && - this.styles[lineIndex][charIndex]); + var styleDeclaration = decl || this._getStyleDeclaration(lineIndex, charIndex); if (styleDeclaration) { // cloning so that original style object is not polluted with following font declarations @@ -917,14 +911,64 @@ }, /** - * @private * @param {Number} lineIndex * @param {Number} charIndex + * @param {Boolean} [returnCloneOrEmpty=false] + * @private */ - _getStyleDeclaration: function(lineIndex, charIndex) { - return (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) - ? clone(this.styles[lineIndex][charIndex]) - : { }; + _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} syle + * @private + */ + _setLineStyle: function(lineIndex, syle) { + this.styles[lineIndex] = style; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _deleteLineStyle: function(lineIndex) { + delete this.styles[lineIndex]; }, /** @@ -936,7 +980,7 @@ return this._getWidthOfSpace(ctx, lineIndex); } - var styleDeclaration = this._getStyleDeclaration(lineIndex, charIndex); + var styleDeclaration = this._getStyleDeclaration(lineIndex, charIndex, true); this._applyFontStyles(styleDeclaration); var cacheProp = this._getCacheProp(_char, styleDeclaration); @@ -956,10 +1000,8 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _getHeightOfChar: function(ctx, _char, lineIndex, charIndex) { - if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) { - return this.styles[lineIndex][charIndex].fontSize || this.fontSize; - } - return this.fontSize; + var style = this._getStyleDeclaration(lineIndex, charIndex); + return style && style.fontSize ? style.fontSize : this.fontSize; }, /** diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index 0cb13641..12a6646d 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -70,9 +70,110 @@ this._setTextStyles(ctx); } this._textLines = this._splitTextIntoLines(); + this._styleMap = this._generateStyleMap(); this._clearCache(); this.height = this._getTextHeight(ctx); }, + + _generateStyleMap: function() { + var realLineCount = 0; + var realLineCharCount = 0; + var charCount = 0; + var map = {}; + + for(var i = 0; i < this._textLines.length; i++) { + if(this.text[charCount] === '\n') { + realLineCharCount = 0; + charCount++; + realLineCount++; + } + + 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]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + } + + 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) { + 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. @@ -83,12 +184,10 @@ * @returns {Array} Array of lines */ _wrapText: function (ctx, text) { - var lines = text.split(this._reNewline), wrapped = [], lineIndex = 0, newLines, i; + var lines = text.split(this._reNewline), wrapped = [], i; for (i = 0; i < lines.length; i++) { - newLines = this._wrapLine(ctx, lines[i], lineIndex); - lineIndex += newLines.length; - wrapped = wrapped.concat(newLines); + wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i)); } return wrapped; @@ -108,8 +207,10 @@ var width = 0, decl; charOffset = charOffset || 0; - for (var i = charOffset; i < charOffset + text.length; i++) { - if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { + for (var i = 0; i < text.length; i++) { + decl = this._getStyleDeclaration(lineIndex, i + charOffset); + + if (decl) { ctx.save(); width += this._applyCharStylesGetWidth(ctx, text[i], lineIndex, i, decl); ctx.restore(); @@ -126,6 +227,7 @@ * 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. */ @@ -134,7 +236,9 @@ lines = [], line = ''; - if (this._measureText(ctx, text, lineIndex) < maxWidth) { + var offset = 0; + + if (this._measureText(ctx, text, lineIndex, offset) < maxWidth) { lines.push(text); } else { @@ -153,7 +257,7 @@ * This handles a word that is longer than the width of the * text area. */ - while (Math.ceil(this._measureText(ctx, words[0], lineIndex)) >= maxWidth) { + while (Math.ceil(this._measureText(ctx, words[0], lineIndex, offset)) >= maxWidth) { var tmp = words[0]; words[0] = tmp.slice(0, -1); @@ -165,11 +269,11 @@ } } - if (Math.ceil(this._measureText(ctx, line + words[0], lineIndex)) < maxWidth) { + if (Math.ceil(this._measureText(ctx, line + words[0], lineIndex, offset)) < maxWidth) { line += words.shift() + ' '; } else { - lineIndex++; + offset += line.length; lines.push(line); line = ''; }