diff --git a/src/controls.actions.js b/src/controls.actions.js index b66aa8cb030..0883853e34d 100644 --- a/src/controls.actions.js +++ b/src/controls.actions.js @@ -690,9 +690,10 @@ strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), multiplier = isTransformCentered(transform) ? 2 : 1, oldWidth = target.width, - newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; + newWidth = Math.ceil(Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding); target.set('width', Math.max(newWidth, 0)); - return oldWidth !== newWidth; + // check against actual target width in case `newWidth` was rejected + return oldWidth !== target.width; } /** diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 4d075b4f4a4..2cb6c093d32 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -432,12 +432,12 @@ */ fromStringToGraphemeSelection: function(start, end, text) { var smallerTextStart = text.slice(0, start), - graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; + graphemeStart = this.graphemeSplit(smallerTextStart).length; if (start === end) { return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; } var smallerTextEnd = text.slice(start, end), - graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; + graphemeEnd = this.graphemeSplit(smallerTextEnd).length; return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; }, diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index d45217fd7c2..509f27cb100 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -679,7 +679,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (end > start) { this.removeStyleFromTo(start, end); } - var graphemes = fabric.util.string.graphemeSplit(text); + var graphemes = this.graphemeSplit(text); this.insertNewStyleBlock(graphemes, start, style); this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); this.text = this._text.join(''); diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 2bb6bc2e5f7..6db8f16e532 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1512,6 +1512,15 @@ this.callSuper('render', ctx); }, + /** + * Override this method to customize grapheme splitting + * @param {string} value + * @returns {string[]} array of graphemes + */ + graphemeSplit: function (value) { + return fabric.util.string.graphemeSplit(value); + }, + /** * Returns the text as an array of lines. * @param {String} text text to split @@ -1523,7 +1532,7 @@ newLine = ['\n'], newText = []; for (var i = 0; i < lines.length; i++) { - newLines[i] = fabric.util.string.graphemeSplit(lines[i]); + newLines[i] = this.graphemeSplit(lines[i]); newText = newText.concat(newLines[i], newLine); } newText.pop(); diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index 06cf7982cfc..ce423a2de81 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -271,7 +271,7 @@ var wrapped = [], i; this.isWrapping = true; for (i = 0; i < lines.length; i++) { - wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); + wrapped.push.apply(wrapped, this._wrapLine(lines[i], i, desiredWidth)); } this.isWrapping = false; return wrapped; @@ -279,13 +279,15 @@ /** * Helper function to measure a string of text, given its lineIndex and charIndex offset - * it gets called when charBounds are not available yet. + * It gets called when charBounds are not available yet. + * Override if necessary + * Use with {@link fabric.Textbox#wordSplit} + * * @param {CanvasRenderingContext2D} ctx * @param {String} text * @param {number} lineIndex * @param {number} charOffset * @returns {number} - * @private */ _measureWord: function(word, lineIndex, charOffset) { var width = 0, prevGrapheme, skipLeft = true; @@ -298,6 +300,16 @@ return width; }, + /** + * Override this method to customize word splitting + * Use with {@link fabric.Textbox#_measureWord} + * @param {string} value + * @returns {string[]} array of words + */ + wordSplit: function (value) { + return value.split(this._wordJoiners); + }, + /** * Wraps a line of text using the width of the Textbox and a context. * @param {Array} line The grapheme array that represent the line @@ -313,7 +325,7 @@ graphemeLines = [], line = [], // spaces in different languages? - words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), + words = splitByGrapheme ? this.graphemeSplit(_line) : this.wordSplit(_line), word = '', offset = 0, infix = splitByGrapheme ? '' : ' ', @@ -328,14 +340,25 @@ words.push([]); } desiredWidth -= reservedSpace; - for (var i = 0; i < words.length; i++) { + // measure words + var data = words.map(function (word) { // if using splitByGrapheme words are already in graphemes. - word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); - wordWidth = this._measureWord(word, lineIndex, offset); + word = splitByGrapheme ? word : this.graphemeSplit(word); + var width = this._measureWord(word, lineIndex, offset); + largestWordWidth = Math.max(width, largestWordWidth); + offset += word.length + 1; + return { word: word, width: width }; + }.bind(this)); + var maxWidth = Math.max(desiredWidth, largestWordWidth, this.dynamicMinWidth); + // layout words + offset = 0; + for (var i = 0; i < words.length; i++) { + word = data[i].word; + wordWidth = data[i].width; offset += word.length; lineWidth += infixWidth + wordWidth - additionalSpace; - if (lineWidth > desiredWidth && !lineJustStarted) { + if (lineWidth > maxWidth && !lineJustStarted) { graphemeLines.push(line); line = []; lineWidth = wordWidth; @@ -353,10 +376,6 @@ infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); offset++; lineJustStarted = false; - // keep track of largest word - if (wordWidth > largestWordWidth) { - largestWordWidth = wordWidth; - } } i && graphemeLines.push(line); diff --git a/test/unit/controls_handlers.js b/test/unit/controls_handlers.js index 9a30bf60b02..f986eca4d58 100644 --- a/test/unit/controls_handlers.js +++ b/test/unit/controls_handlers.js @@ -21,11 +21,22 @@ }); QUnit.test('changeWidth changes the width', function(assert) { assert.equal(transform.target.width, 100); - fabric.controlsUtils.changeWidth(eventData, transform, 200, 300); + var changed = fabric.controlsUtils.changeWidth(eventData, transform, 200, 300); + assert.ok(changed, 'control changed target'); assert.equal(transform.target.width, 199); assert.equal(transform.target.left, 0); assert.equal(transform.target.top, 0); }); + QUnit.test('changeWidth does not change the width', function (assert) { + var target = new fabric.Rect({ width: 100, height: 100, canvas }); + target._set = () => { }; + assert.equal(target.width, 100); + var changed = fabric.controlsUtils.changeWidth(eventData, Object.assign({}, transform, { target }), 200, 300); + assert.ok(!changed, 'control change was rejected'); + assert.equal(target.width, 100); + assert.equal(target.left, 0); + assert.equal(target.top, 0); + }); QUnit.test('changeWidth changes the width with centered transform', function(assert) { transform.originX = 'center'; transform.originY = 'center'; @@ -51,13 +62,13 @@ transform.target.strokeUniform = true; transform.target.scaleX = 3; fabric.controlsUtils.changeWidth(eventData, transform, 200, 300); - assert.equal(Math.floor(transform.target.width), 61); + assert.equal(Math.ceil(transform.target.width), 62); }); QUnit.test('changeWidth changes the width with big strokeWidth + scaling', function(assert) { transform.target.strokeWidth = 15; transform.target.scaleX = 3; fabric.controlsUtils.changeWidth(eventData, transform, 200, 300); - assert.equal(Math.floor(transform.target.width), 51); + assert.equal(Math.ceil(transform.target.width), 52); }); QUnit.test('changeWidth will fire events on canvas and target resizing', function(assert) { var done = assert.async(); diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 90f8ff75461..636b48dd5fa 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -440,58 +440,31 @@ assert.deepEqual(textbox.styles[0], {}, 'style is an empty object'); }); - QUnit.test('_deleteStyleDeclaration', function(assert) { - var textbox = new fabric.Textbox('aaa aaq ggg gg oee eee', { - styles: { - 0: { - 0: { fontSize: 4 }, - 1: { fontSize: 4 }, - 2: { fontSize: 4 }, - 3: { fontSize: 4 }, - 4: { fontSize: 4 }, - 5: { fontSize: 4 }, - 6: { fontSize: 4 }, - 7: { fontSize: 4 }, - 8: { fontSize: 4 }, - 9: { fontSize: 4 }, - 10: { fontSize: 4 }, - 11: { fontSize: 4 }, - 12: { fontSize: 4 }, - 13: { fontSize: 4 }, - 14: { fontSize: 4 }, - 15: { fontSize: 4 }, - 16: { fontSize: 4 }, - }, - }, + QUnit.test('_deleteStyleDeclaration', function (assert) { + var text = 'aaa aaq ggg gg oee eee'; + var styles = {}; + for (var index = 0; index < text.length; index++) { + styles[index] = { fontSize: 4 }; + + } + var textbox = new fabric.Textbox(text, { + styles: { 0: styles }, width: 5, }); + assert.equal(typeof textbox._deleteStyleDeclaration, 'function', 'function exists'); textbox._deleteStyleDeclaration(2, 2); assert.equal(textbox.styles[0][10], undefined, 'style has been removed'); }); QUnit.test('_setStyleDeclaration', function(assert) { - var textbox = new fabric.Textbox('aaa aaq ggg gg oee eee', { - styles: { - 0: { - 0: { fontSize: 4 }, - 1: { fontSize: 4 }, - 2: { fontSize: 4 }, - 3: { fontSize: 4 }, - 4: { fontSize: 4 }, - 5: { fontSize: 4 }, - 6: { fontSize: 4 }, - 7: { fontSize: 4 }, - 8: { fontSize: 4 }, - 9: { fontSize: 4 }, - 10: { fontSize: 4 }, - 11: { fontSize: 4 }, - 12: { fontSize: 4 }, - 13: { fontSize: 4 }, - 14: { fontSize: 4 }, - 15: { fontSize: 4 }, - 16: { fontSize: 4 }, - }, - }, + var text = 'aaa aaq ggg gg oee eee'; + var styles = {}; + for (var index = 0; index < text.length; index++) { + styles[index] = { fontSize: 4 }; + + } + var textbox = new fabric.Textbox(text, { + styles: { 0: styles }, width: 5, }); assert.equal(typeof textbox._setStyleDeclaration, 'function', 'function exists');