From 3b10702512ed05f384cb6bdd2ba40b2888b096da Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 15 Aug 2017 10:10:01 +0200 Subject: [PATCH] Normalize Api for getSelectionStyles, setSelectionStyles (#4202) * reworked the text selection * reorganized api * missing file * fixed lint * more test --- build.js | 1 + src/mixins/text_style.mixin.js | 310 +++++++++++++++++++++++++++++++++ src/shapes/itext.class.js | 69 -------- src/shapes/text.class.js | 222 ----------------------- test/unit/itext.js | 41 +---- test/unit/text.js | 149 ++++++++++++++++ 6 files changed, 467 insertions(+), 325 deletions(-) create mode 100644 src/mixins/text_style.mixin.js diff --git a/build.js b/build.js index bc7b928d6f9..9df0f25f02e 100644 --- a/build.js +++ b/build.js @@ -224,6 +224,7 @@ var filesToInclude = [ ifSpecifiedInclude('image_filters', 'src/filters/hue_rotation.class.js'), ifSpecifiedInclude('text', 'src/shapes/text.class.js'), + ifSpecifiedInclude('text', 'src/mixins/text_style.mixin.js'), ifSpecifiedInclude('itext', 'src/shapes/itext.class.js'), ifSpecifiedInclude('itext', 'src/mixins/itext_behavior.mixin.js'), diff --git a/src/mixins/text_style.mixin.js b/src/mixins/text_style.mixin.js new file mode 100644 index 00000000000..704bdcdb8fd --- /dev/null +++ b/src/mixins/text_style.mixin.js @@ -0,0 +1,310 @@ +(function() { + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return true; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + 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; + }, + + /** + * Returns true if object has a style property or has it ina specified line + * @param {Number} lineIndex + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (!this.styles || !property || property === '') { + return false; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return false; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + // eslint-disable-next-line + for (var p1 in obj) { + // eslint-disable-next-line + for (var p2 in obj[p1]) { + if (typeof obj[p1][p2][property] !== 'undefined') { + return true; + } + } + } + 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) { + 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 + */ + _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); + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. usefull to manage styles. + */ + get2DCursorLocation: function(selectionStart, skipWrapping) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var lines = skipWrapping ? this._unwrappedTextLines : this._textLines; + var len = lines.length; + for (var i = 0; i < len; i++) { + if (selectionStart <= lines[i].length) { + return { + lineIndex: i, + charIndex: selectionStart + }; + } + selectionStart -= lines[i].length + 1; + } + return { + lineIndex: i - 1, + charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart + }; + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * if startIndex or endIndex are not provided, slectionStart or selectionEnd will be used. + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @param {Boolean} [complete] get full style or not + * @return {Array} styles an array with one, zero or more Style objects + */ + getSelectionStyles: function(startIndex, endIndex, complete) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + var styles = []; + for (var i = startIndex; i < endIndex; i++) { + styles.push(this.getStyleAtPosition(i, complete)); + } + return styles; + }, + + /** + * Gets style of a current selection/cursor position + * @param {Number} position to get styles at + * @param {Boolean} [complete] full style if true + * @return {Object} style Style object at a specified index + * @private + */ + getStyleAtPosition: function(position, complete) { + var loc = this.get2DCursorLocation(position), + style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : + this._getStyleDeclaration(loc.lineIndex, loc.charIndex); + return style || {}; + }, + + /** + * Sets style of a current selection, if no selection exist, do not set anything. + * @param {Object} [styles] Styles object + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles, startIndex, endIndex) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + for (var i = startIndex; i < endIndex; i++) { + this._extendStyles(i, styles); + } + /* not included in _extendStyles to avoid clearing cache more than once */ + this._forceClearCache = true; + return this; + }, + + /** + * get the reference, not a clone, of the style object for a given character + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Object} style object + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + var lineStyle = this.styles && this.styles[lineIndex]; + if (!lineStyle) { + return null; + } + return lineStyle[charIndex]; + }, + + /** + * return a new object that contains all the style property for a character + * the object returned is newly created + * @param {Number} lineIndex of the line where the character is + * @param {Number} charIndex position of the character on the line + * @return {Object} style object + */ + getCompleteStyleDeclaration: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex) || { }, + styleObject = { }, prop; + for (var i = 0; i < this._styleProperties.length; i++) { + prop = this._styleProperties[i]; + styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; + } + return styleObject; + }, + + /** + * @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]; + } + }); +})(); diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 4efbd9ee70a..5c597603649 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -223,50 +223,6 @@ 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 - * @param {Boolean} [endIndex] End index to get styles at - * @return {Object} styles Style object at a specified (or current) index - */ - getSelectionStyles: function(startIndex, endIndex, complete) { - - if (endIndex && startIndex !== endIndex) { - var styles = []; - for (var i = startIndex; i < endIndex; i++) { - styles.push(this.getSelectionStyles(i, i, complete)); - } - return styles; - } - - var loc = this.get2DCursorLocation(startIndex), - style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : - this._getStyleDeclaration(loc.lineIndex, loc.charIndex); - - return style || {}; - }, - - /** - * Sets style of a current selection, if no selection exist, do not set anything. - * @param {Object} [styles] Styles object - * @return {fabric.IText} thisArg - * @chainable - */ - setSelectionStyles: function(styles) { - if (this.selectionStart === this.selectionEnd) { - return this; - } - 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; - }, - /** * Initialize text dimensions. Render all text on given context * or on a offscreen canvas to get the text width with measureText. @@ -350,31 +306,6 @@ 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. - * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. usefull to manage styles. - */ - get2DCursorLocation: function(selectionStart, skipWrapping) { - if (typeof selectionStart === 'undefined') { - selectionStart = this.selectionStart; - } - var lines = skipWrapping ? this._unwrappedTextLines : this._textLines; - var len = lines.length; - for (var i = 0; i < len; i++) { - if (selectionStart <= lines[i].length) { - return { - lineIndex: i, - charIndex: selectionStart - }; - } - selectionStart -= lines[i].length + 1; - } - return { - lineIndex: i - 1, - charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart - }; - }, /** * Returns cursor boundaries (left, top, leftOffset, topOffset) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 2033e58ff08..940b2a9c8df 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -309,152 +309,6 @@ return fabric._measuringContext; }, - /** - * Returns true if object has no styling or no styling in a line - * @param {Number} lineIndex - * @return {Boolean} - */ - isEmptyStyles: function(lineIndex) { - if (!this.styles) { - return true; - } - if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { - return true; - } - var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; - 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; - }, - - /** - * Returns true if object has a style property or has it ina specified line - * @param {Number} lineIndex - * @return {Boolean} - */ - styleHas: function(property, lineIndex) { - if (!this.styles || !property || property === '') { - return false; - } - if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { - return false; - } - var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; - // eslint-disable-next-line - for (var p1 in obj) { - // eslint-disable-next-line - for (var p2 in obj[p1]) { - if (typeof obj[p1][p2][property] !== 'undefined') { - return true; - } - } - } - 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) { - 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 - */ - _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 or update text dimensions. * Updates this.width and this.height with the proper values. @@ -700,82 +554,6 @@ ctx.font = this._getFontDeclaration(styleDeclaration); }, - /** - * get the reference, not a clone, of the style object for a given character - * @param {Number} lineIndex - * @param {Number} charIndex - * @return {Object} style object - */ - _getStyleDeclaration: function(lineIndex, charIndex) { - var lineStyle = this.styles && this.styles[lineIndex]; - if (!lineStyle) { - return null; - } - return lineStyle[charIndex]; - }, - - /** - * return a new object that contains all the style property for a character - * the object returned is newly created - * @param {Number} lineIndex of the line where the character is - * @param {Number} charIndex position of the character on the line - * @return {Object} style object - */ - getCompleteStyleDeclaration: function(lineIndex, charIndex) { - var style = this._getStyleDeclaration(lineIndex, charIndex) || { }, - styleObject = { }, prop; - for (var i = 0; i < this._styleProperties.length; i++) { - prop = this._styleProperties[i]; - styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; - } - return styleObject; - }, - - /** - * @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]; - }, - /** * measure and return the width of a single character. * possibly overridden to accommodate different measure logic or diff --git a/test/unit/itext.js b/test/unit/itext.js index af174e2454d..16eb1c96108 100644 --- a/test/unit/itext.js +++ b/test/unit/itext.js @@ -573,48 +573,21 @@ iText.selectionStart = 0; iText.selectionEnd = 0; - deepEqual(iText.getSelectionStyles(), { - textDecoration: 'underline' - }); + deepEqual(iText.getSelectionStyles(), []); iText.selectionStart = 2; - iText.selectionEnd = 2; + iText.selectionEnd = 3; - deepEqual(iText.getSelectionStyles(), { + deepEqual(iText.getSelectionStyles(), [{ textDecoration: 'overline' - }); + }]); iText.selectionStart = 17; - iText.selectionStart = 17; + iText.selectionEnd = 18; - deepEqual(iText.getSelectionStyles(), { + deepEqual(iText.getSelectionStyles(), [{ fill: 'red' - }); - }); - - test('getSelectionStyles with 1 arg', function() { - - var iText = new fabric.IText('test foo bar-baz\nqux', { - styles: { - 0: { - 0: { textDecoration: 'underline' }, - 2: { textDecoration: 'overline' }, - 4: { textBackgroundColor: '#ffc' } - }, - 1: { - 0: { fill: 'red' }, - 1: { fill: 'green' }, - 2: { fill: 'blue' } - } - } - }); - - iText.selectionStart = 17; - iText.selectionStart = 17; - - deepEqual(iText.getSelectionStyles(2), { - textDecoration: 'overline' - }); + }]); }); test('getSelectionStyles with 2 args', function() { diff --git a/test/unit/text.js b/test/unit/text.js index 2f825a6f076..d92c7fb5b89 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -381,5 +381,154 @@ var cache2 = text2.getFontCache(text2); equal(cache, cache2, 'you get the same cache'); }); +// moved + test('getSelectionStyles with no arguments', function() { + var iText = new fabric.Text('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { textDecoration: 'underline' }, + 2: { textDecoration: 'overline' }, + 4: { textBackgroundColor: '#ffc' } + }, + 1: { + 0: { fill: 'red' }, + 1: { fill: 'green' }, + 2: { fill: 'blue' } + } + } + }); + + equal(typeof iText.getSelectionStyles, 'function'); + + deepEqual(iText.getSelectionStyles(), []); + + }); + + test('getSelectionStyles with 2 args', function() { + var iText = new fabric.Text('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { textDecoration: 'underline' }, + 2: { textDecoration: 'overline' }, + 4: { textBackgroundColor: '#ffc' } + }, + 1: { + 0: { fill: 'red' }, + 1: { fill: 'green' }, + 2: { fill: 'blue' } + } + } + }); + + deepEqual(iText.getSelectionStyles(0, 5), [ + { textDecoration: 'underline' }, + {}, + { textDecoration: 'overline' }, + {}, + { textBackgroundColor: '#ffc' }, + ]); + + deepEqual(iText.getSelectionStyles(2, 2), [ + ]); + }); + test('setSelectionStyles', function() { + var iText = new fabric.Text('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { fill: '#112233' }, + 2: { stroke: '#223344' } + } + } + }); + + equal(typeof iText.setSelectionStyles, 'function'); + + iText.setSelectionStyles({ + fill: 'red', + stroke: 'yellow' + }); + + deepEqual(iText.styles[0][0], { + fill: '#112233' + }); + + iText.setSelectionStyles({ + fill: 'red', + stroke: 'yellow' + }, 0, 1); + + deepEqual(iText.styles[0][0], { + fill: 'red', + stroke: 'yellow' + }); + + iText.setSelectionStyles({ + fill: '#998877', + stroke: 'yellow' + }, 2, 3); + + deepEqual(iText.styles[0][2], { + fill: '#998877', + stroke: 'yellow' + }); + }); + + test('getStyleAtPosition', function() { + var iText = new fabric.Text('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { textDecoration: 'underline' }, + 2: { textDecoration: 'overline' }, + 4: { textBackgroundColor: '#ffc' } + }, + 1: { + 0: { fill: 'red' }, + 1: { fill: 'green' }, + 2: { fill: 'blue' } + } + } + }); + + equal(typeof iText.getStyleAtPosition, 'function'); + + deepEqual(iText.getStyleAtPosition(2), { textDecoration: 'overline' }); + + deepEqual(iText.getStyleAtPosition(1), { }); + + deepEqual(iText.getStyleAtPosition(18), { fill: 'green' }); + }); + + test('getStyleAtPosition complete', function() { + var iText = new fabric.Text('test foo bar-baz\nqux', { + styles: { + 0: { + 0: { textDecoration: 'underline' }, + 2: { textDecoration: 'overline' }, + 4: { textBackgroundColor: '#ffc' } + }, + 1: { + 0: { fill: 'red' }, + 1: { fill: 'green' }, + 2: { fill: 'blue' } + } + } + }); + + equal(typeof iText.getStyleAtPosition, 'function'); + + deepEqual(iText.getStyleAtPosition(2, true), { + stroke: null, + strokeWidth: 1, + fill: 'rgb(0,0,0)', + fontFamily: 'Times New Roman', + fontSize: 40, + fontWeight: 'normal', + fontStyle: 'normal', + underline: false, + overline: false, + linethrough: false, + textBackgroundColor: '' + }); + }); })();