diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index ddb5c0dbcd2..f60bafe54c2 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -492,7 +492,7 @@ * @return {Boolean} */ styleHas: function(property, lineIndex) { - if (!this.styles) { + if (!this.styles || !property || property === '') { return false; } if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { @@ -511,6 +511,86 @@ return false; }, + /** + * Check if characters in a text have a value for a property + * whose value matches the textbox's value for that property. If so, + * the character-level property is deleted. If the character + * has no other properties, then it is also deleted. Finally, + * if the line containing that character has no other characters + * then it also is deleted. + * + * @param {string} property The property to compare between characters and text. + */ + cleanStyle: function(property) { + if (!this.styles || !property || property === '') { + return false; + } + var obj = this.styles, stylesCount = 0, letterCount, foundStyle = false, style, + canBeSwapped = true, graphemeCount = 0; + // eslint-disable-next-line + for (var p1 in obj) { + letterCount = 0; + // eslint-disable-next-line + for (var p2 in obj[p1]) { + stylesCount++; + if (!foundStyle) { + style = obj[p1][p2][property]; + foundStyle = true; + } + else if (obj[p1][p2][property] !== style) { + canBeSwapped = false; + } + if (obj[p1][p2][property] === this[property]) { + delete obj[p1][p2][property]; + } + if (Object.keys(obj[p1][p2]).length !== 0) { + letterCount++; + } + else { + delete obj[p1][p2]; + } + } + if (letterCount === 0) { + delete obj[p1]; + } + } + // if every grapheme has the same style set then + // delete those styles and set it on the parent + for (var i = 0; i < this._textLines.length; i++) { + graphemeCount += this._textLines[i].length; + } + if (canBeSwapped && stylesCount === graphemeCount) { + this[property] = style; + this.removeStyle(property); + } + }, + + /** + * Remove a style property or properties from all individual character styles + * in a text object. Deletes the character style object if it contains no other style + * props. Deletes a line style object if it contains no other character styles. + * + * @param {String} props The property to remove from character styles. + */ + removeStyle: function(property) { + if (!this.styles || !property || property === '') { + return; + } + var obj = this.styles, line, lineNum, charNum; + for (lineNum in obj) { + var line = obj[lineNum]; + for (charNum in line) { + delete line[charNum][property]; + if (Object.keys(line[charNum]).length === 0) { + delete line[charNum]; + } + } + if (Object.keys(line).length === 0) { + delete obj[lineNum]; + } + } + }, + /** * @private */ diff --git a/test/unit/text.js b/test/unit/text.js index a600f29d459..eca9e615337 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -332,4 +332,58 @@ equal(removeTranslate(text.toSVG()), removeTranslate(TEXT_SVG_JUSTIFIED)); }); + test('text styleHas', function() { + var text = new fabric.Text('xxxxxx\nx y'); + text.styles = { }; + ok(typeof text.styleHas === 'function'); + equal(text.styleHas('stroke'), false, 'the text style has no stroke'); + text.styles = { 1: { 0: { stroke: 'red' }}}; + equal(text.styleHas('stroke'), true, 'the text style has stroke'); + }); + + test('text cleanStyle', function() { + var text = new fabric.Text('xxxxxx\nx y'); + text.styles = { 1: { 0: { stroke: 'red' }}}; + text.stroke = 'red'; + ok(typeof text.cleanStyle === 'function'); + text.cleanStyle('stroke'); + equal(text.styles[1], undefined, 'the style has been cleaned since stroke was equal to text property'); + text.styles = { 1: { 0: { stroke: 'blue' }}}; + text.stroke = 'red'; + text.cleanStyle('stroke'); + equal(text.styles[1][0].stroke, 'blue', 'nothing to clean, style untouched'); + }); + + test('text cleanStyle with empty styles', function() { + var text = new fabric.Text('xxxxxx\nx y'); + text.styles = { 1: { 0: { }, 1: { }}, 2: { }, 3: { 4: { }}}; + text.cleanStyle('any'); + equal(text.styles[1], undefined, 'the style has been cleaned since there were no usefull informations'); + equal(text.styles[2], undefined, 'the style has been cleaned since there were no usefull informations'); + equal(text.styles[3], undefined, 'the style has been cleaned since there were no usefull informations'); + }); + + test('text cleanStyle with full style', function() { + var text = new fabric.Text('xxx'); + text.styles = { 0: { 0: { fill: 'blue' }, 1: { fill: 'blue' }, 2: { fill: 'blue' }}}; + text.fill = 'black'; + text.cleanStyle('fill'); + equal(text.fill, 'blue', 'the fill has been changed to blue'); + equal(text.styles[0], undefined, 'all the style has been removed'); + }); + + test('text removeStyle with some style', function() { + var text = new fabric.Text('xxx'); + text.styles = { 0: { 0: { stroke: 'black', fill: 'blue' }, 1: { fill: 'blue' }, 2: { fill: 'blue' }}}; + ok(typeof text.removeStyle === 'function'); + text.fill = 'red'; + text.removeStyle('fill'); + equal(text.fill, 'red', 'the fill has not been changed'); + equal(text.styles[0][0].stroke, 'black', 'the non fill part of the style is still there'); + equal(text.styles[0][0].fill, undefined, 'the fill part of the style has been removed'); + text.styles = { 0: { 0: { fill: 'blue' }, 1: { fill: 'blue' }, 2: { fill: 'blue' }}}; + text.removeStyle('fill'); + equal(text.styles[0], undefined, 'the styles got empty and has been removed'); + }); + })();