diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 9ded51c0747..174481fccb3 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -200,7 +200,7 @@ * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findWordBoundaryLeft: function(startFrom) { + findWordBoundaryStart: function(startFrom) { var offset = 0, index = startFrom - 1; // remove space before cursor first @@ -215,7 +215,7 @@ index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -223,7 +223,7 @@ * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findWordBoundaryRight: function(startFrom) { + findWordBoundaryEnd: function(startFrom) { var offset = 0, index = startFrom; // remove space after cursor first @@ -246,7 +246,7 @@ * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findLineBoundaryLeft: function(startFrom) { + findLineBoundaryStart: function(startFrom) { var offset = 0, index = startFrom - 1; while (!/\n/.test(this._text[index]) && index > -1) { @@ -254,7 +254,7 @@ index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -262,7 +262,7 @@ * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findLineBoundaryRight: function(startFrom) { + findLineBoundaryEnd: function(startFrom) { var offset = 0, index = startFrom; while (!/\n/.test(this._text[index]) && index < this._text.length) { @@ -320,8 +320,8 @@ */ selectLine: function(selectionStart) { selectionStart = selectionStart || this.selectionStart; - var newSelectionStart = this.findLineBoundaryLeft(selectionStart), - newSelectionEnd = this.findLineBoundaryRight(selectionStart); + var newSelectionStart = this.findLineBoundaryStart(selectionStart), + newSelectionEnd = this.findLineBoundaryEnd(selectionStart); this.selectionStart = newSelectionStart; this.selectionEnd = newSelectionEnd; @@ -397,19 +397,19 @@ currentStart = this.selectionStart, currentEnd = this.selectionEnd; if ( - (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) + (newSelectionStart !== this.__selectionStartOrigin || currentStart === currentEnd) && (currentStart === newSelectionStart || currentEnd === newSelectionStart) ) { return; } - if (newSelectionStart > this.__selectionStartOnMouseDown) { - this.selectionStart = this.__selectionStartOnMouseDown; + if (newSelectionStart > this.__selectionStartOrigin) { + this.selectionStart = this.__selectionStartOrigin; this.selectionEnd = newSelectionStart; } else { this.selectionStart = newSelectionStart; - this.selectionEnd = this.__selectionStartOnMouseDown; + this.selectionEnd = this.__selectionStartOrigin; } if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { this.restartCursorIfNeeded(); @@ -455,11 +455,15 @@ var smallerTextStart = _text.slice(0, start), graphemeStart = smallerTextStart.join('').length; if (start === end) { - return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart, selectionDirection: 'forward' }; } var smallerTextEnd = _text.slice(start, end), graphemeEnd = smallerTextEnd.join('').length; - return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + return { + selectionStart: graphemeStart, + selectionEnd: graphemeStart + graphemeEnd, + selectionDirection: graphemeStart < this.__selectionStartOrigin ? 'backward' : 'forward' + }; }, /** @@ -472,8 +476,12 @@ } if (!this.inCompositionMode) { var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text); - this.hiddenTextarea.selectionStart = newSelection.selectionStart; - this.hiddenTextarea.selectionEnd = newSelection.selectionEnd; + this.hiddenTextarea.setSelectionRange( + newSelection.selectionStart, + newSelection.selectionEnd, + newSelection.selectionDirection + ); + this.selectionDirection = newSelection.selectionDirection; } this.updateTextareaPosition(); }, @@ -497,6 +505,7 @@ if (!this.inCompositionMode) { this.selectionStart = newSelection.selectionStart; } + this.selectionDirection = newSelection.selectionDirection; this.updateTextareaPosition(); }, @@ -900,14 +909,14 @@ if (end === start) { this._selectionDirection = 'left'; } - else if (this._selectionDirection === 'right') { + else if (this.selectionDirection === 'forward') { this._selectionDirection = 'left'; this.selectionEnd = start; } this.selectionStart = newSelection; } else if (newSelection > start && newSelection < end) { - if (this._selectionDirection === 'right') { + if (this.selectionDirection === 'forward') { this.selectionEnd = newSelection; } else { @@ -919,7 +928,7 @@ if (end === start) { this._selectionDirection = 'right'; } - else if (this._selectionDirection === 'left') { + else if (this.selectionDirection === 'backward') { this._selectionDirection = 'right'; this.selectionStart = end; } diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index b1bac2b8895..8c6214889ff 100644 --- a/src/mixins/itext_click_behavior.mixin.js +++ b/src/mixins/itext_click_behavior.mixin.js @@ -109,7 +109,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } if (this.isEditing) { - this.__selectionStartOnMouseDown = this.selectionStart; + this.__selectionStartOrigin = this.selectionStart; if (this.selectionStart === this.selectionEnd) { this.abortCursorAnimation(); } diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 8372dbd44f0..53a8d77c7f0 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -58,11 +58,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorRight', - 36: 'moveCursorLeft', - 37: 'moveCursorLeft', + 35: 'moveCursorForward', + 36: 'moveCursorBackward', + 37: 'moveCursorBackward', 38: 'moveCursorUp', - 39: 'moveCursorRight', + 39: 'moveCursorForward', 40: 'moveCursorDown', }, @@ -71,11 +71,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorLeft', - 36: 'moveCursorRight', - 37: 'moveCursorRight', + 35: 'moveCursorBackward', + 36: 'moveCursorForward', + 37: 'moveCursorForward', 38: 'moveCursorUp', - 39: 'moveCursorLeft', + 39: 'moveCursorBackward', 40: 'moveCursorDown', }, @@ -456,7 +456,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // getUpCursorOffset // getDownCursorOffset var action = 'get' + direction + 'CursorOffset', - offset = this[action](e, this._selectionDirection === 'right'); + offset = this[action](e, this.selectionDirection === 'forward'); if (e.shiftKey) { this.moveCursorWithShift(offset); } @@ -478,7 +478,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @param {Number} offset */ moveCursorWithShift: function(offset) { - var newSelection = this._selectionDirection === 'left' + var newSelection = this.selectionDirection === 'backward' ? this.selectionStart + offset : this.selectionEnd + offset; this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); @@ -502,111 +502,86 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor left + * Moves cursor back * @param {Event} e Event object */ - moveCursorLeft: function(e) { + moveCursorBackward: function (e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } - this._moveCursorLeftOrRight('Left', e); - }, - - /** - * @private - * @return {Boolean} true if a change happened - */ - _move: function(e, prop, direction) { - var newValue; - if (e.altKey) { - newValue = this['findWordBoundary' + direction](this[prop]); - } - else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { - newValue = this['findLineBoundary' + direction](this[prop]); + var changed = false; + if (e.shiftKey) { + if (this.selectionDirection === 'forward' && this.selectionStart !== this.selectionEnd) { + changed = this._move(e, 'selectionEnd', -1); + } + else if (this.selectionStart !== 0) { + //this._selectionDirection = 'left'; + this.__selectionStartOrigin = this.selectionEnd; + changed = this._move(e, 'selectionStart', -1); + } } else { - this[prop] += direction === 'Left' ? -1 : 1; - return true; - } - if (typeof newValue !== undefined && this[prop] !== newValue) { - this[prop] = newValue; - return true; - } - }, - - /** - * @private - */ - _moveLeft: function(e, prop) { - return this._move(e, prop, 'Left'); - }, - - /** - * @private - */ - _moveRight: function(e, prop) { - return this._move(e, prop, 'Right'); - }, - - /** - * Moves cursor left without keeping selection - * @param {Event} e - */ - moveCursorLeftWithoutShift: function(e) { - var change = true; - this._selectionDirection = 'left'; - - // only move cursor when there is no selection, - // otherwise we discard it, and leave cursor on same place - if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) { - change = this._moveLeft(e, 'selectionStart'); - - } - this.selectionEnd = this.selectionStart; - return change; - }, - - /** - * Moves cursor left while keeping selection - * @param {Event} e - */ - moveCursorLeftWithShift: function(e) { - if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { - return this._moveLeft(e, 'selectionEnd'); - } - else if (this.selectionStart !== 0){ - this._selectionDirection = 'left'; - return this._moveLeft(e, 'selectionStart'); + changed = true; + //this._selectionDirection = 'left'; + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart) { + changed = this.selectionStart !== 0 && this._move(e, 'selectionStart', -1); + this.selectionEnd = this.selectionStart; + } + else if (this.selectionDirection === 'forward') { + this.selectionStart = this.selectionEnd; + } + else { + this.selectionEnd = this.selectionStart; + } } + this._invalidateCursor(changed); }, /** - * Moves cursor right + * Moves cursor forward * @param {Event} e Event object */ - moveCursorRight: function(e) { + moveCursorForward: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - this._moveCursorLeftOrRight('Right', e); + var changed = false; + if (e.shiftKey) { + if (this.selectionDirection === 'backward' && this.selectionStart !== this.selectionEnd) { + changed = this._move(e, 'selectionStart', 1); + } + else if (this.selectionEnd !== this._text.length) { + //this._selectionDirection = 'right'; + this.__selectionStartOrigin = this.selectionStart; + changed = this._move(e, 'selectionEnd', 1); + } + } + else { + changed = true; + //this._selectionDirection = 'right'; + if (this.selectionStart === this.selectionEnd) { + changed = this._move(e, 'selectionStart', 1); + this.selectionEnd = this.selectionStart; + } + else if (this.selectionDirection === 'forward') { + this.selectionStart = this.selectionEnd; + } + else { + this.selectionEnd = this.selectionStart; + } + } + this._invalidateCursor(changed); }, /** - * Moves cursor right or Left, fires event - * @param {String} direction 'Left', 'Right' - * @param {Event} e Event object + * @private + * @param {boolean} dirty */ - _moveCursorLeftOrRight: function(direction, e) { - var actionName = 'moveCursor' + direction + 'With'; + _invalidateCursor: function (dirty) { this._currentCursorOpacity = 1; - - if (e.shiftKey) { - actionName += 'Shift'; - } - else { - actionName += 'outShift'; - } - if (this[actionName](e)) { + if (dirty) { this.abortCursorAnimation(); this.initDelayedCursor(); this._fireSelectionChanged(); @@ -615,35 +590,36 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor right while keeping selection - * @param {Event} e + * @private + * @param {Event} e + * @param {'selectionStart'|'selectionEnd'} prop + * @param {number} direction + * @returns {boolean} true if a change happened */ - moveCursorRightWithShift: function(e) { - if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { - return this._moveRight(e, 'selectionStart'); + _move: function(e, prop, direction) { + var newValue; + direction = Math.sign(direction); + if (direction === 0) { + return false; } - else if (this.selectionEnd !== this._text.length) { - this._selectionDirection = 'right'; - return this._moveRight(e, 'selectionEnd'); + if (e.altKey) { + newValue = direction > 0 ? + this.findWordBoundaryEnd(this[prop]) : + this.findWordBoundaryStart(this[prop]); } - }, - - /** - * Moves cursor right without keeping selection - * @param {Event} e Event object - */ - moveCursorRightWithoutShift: function(e) { - var changed = true; - this._selectionDirection = 'right'; - - if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); - this.selectionEnd = this.selectionStart; + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = direction > 0 ? + this.findLineBoundaryEnd(this[prop]) : + this.findLineBoundaryStart(this[prop]); } else { - this.selectionStart = this.selectionEnd; + this[prop] = Math.min(Math.max(this[prop] + direction, 0), this.text.length); + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; } - return changed; }, /** diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 8040e65f15b..3c3e1cb5275 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -68,6 +68,15 @@ */ selectionEnd: 0, + /** + * Selection direction relative to initial selection start. + * Same as HTMLTextareaElement#selectionDirection + * @typedef {'forward' | 'backward' | 'none'} SelectionDirection + * @type {SelectionDirection} + * @default + */ + selectionDirection: 'forward', + /** * Color of text selection * @type String @@ -219,6 +228,45 @@ this._updateAndFire('selectionEnd', index); }, + /** + * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange} + * @param {number} selectionStart + * @param {number} selectionEnd + * @param {SelectionDirection} [selectionDirection] + */ + setSelectionRange: function (selectionStart, selectionEnd, selectionDirection) { + this._setSelectionRange(selectionStart, selectionEnd, selectionDirection || 'none'); + }, + + /** + * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange} + * @private + * @param {number} selectionStart + * @param {number} selectionEnd + * @param {SelectionDirection|false} [selectionDirection] pass `false` to preserve current `selectionDirection` value + */ + _setSelectionRange: function (selectionStart, selectionEnd, selectionDirection) { + selectionStart = Math.max(selectionStart, 0); + selectionEnd = Math.min(selectionEnd, this.text.length); + if (selectionStart > selectionEnd) { + // mimic HTMLTextareaElement behavior + selectionStart = selectionEnd; + } + var changed = selectionStart !== this.selectionStart || selectionEnd !== this.selectionEnd; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + if (selectionDirection !== false) { + // mimic HTMLTextareaElement behavior + this.selectionDirection = selectionDirection === 'backward' ? 'backward' : 'forward'; + // needed for future calcualtions of `selectionDirection` + this.__selectionStartOrigin = this.selectionDirection === 'forward' ? + this.selectionStart : + this.selectionEnd; + } + changed && this._fireSelectionChanged(); + this._updateTextarea(); + }, + /** * @private * @param {String} property 'selectionStart' or 'selectionEnd' @@ -371,14 +419,30 @@ left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { - boundaries.left *= -1; - } - else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { - boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); - } - else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { - boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + switch (this.textAlign) { + case 'start': + case 'justify': + case 'justify-start': + boundaries.left *= -1; + break; + case 'end': + case 'justify-end': + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + break; + case 'left': + case 'justify-left': + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + break; + case 'center': + case 'justify-center': + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + break; + case 'right': + case 'justify-right': + boundaries.left *= -1; + break; + default: + break; } } this.cursorOffsetCache = boundaries; @@ -468,14 +532,30 @@ ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { - drawStart = this.width - drawStart - drawWidth; - } - else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { - drawStart = boundaries.left + lineOffset - boxEnd; - } - else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { - drawStart = boundaries.left + lineOffset - boxEnd; + switch (this.textAlign) { + case 'start': + case 'justify': + case 'justify-start': + drawStart = this.width - drawStart - drawWidth; + break; + case 'end': + case 'justify-end': + drawStart = boundaries.left + lineOffset - boxEnd; + break; + case 'left': + case 'justify-left': + drawStart = boundaries.left + lineOffset - boxEnd; + break; + case 'center': + case 'justify-center': + drawStart = boundaries.left + lineOffset - boxEnd; + break; + case 'right': + case 'justify-right': + drawStart = this.width - drawStart - drawWidth; + break; + default: + break; } } ctx.fillRect( diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 1d99be796cf..639ecb3c40e 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -122,12 +122,12 @@ linethrough: false, /** - * Text alignment. Possible values: "left", "center", "right", "justify", - * "justify-left", "justify-center" or "justify-right". - * @type String + * Text alignment. + * @typedef {'start' | 'center' | 'end' | 'left' | 'right' | 'justify' | 'justify-start' | 'justify-center' | 'justify-end' | 'justify-left' | 'justify-right'} TextAlign + * @type {TextAlign} * @default */ - textAlign: 'left', + textAlign: 'start', /** * Font style . Possible values: "", "normal", "italic" or "oblique". @@ -235,10 +235,10 @@ /** * Which side of the path the text should be drawn on. * Only used when text has a path - * @type {String} 'left|right' + * @type {'left'|'right'|'start'|'end'} * @default */ - pathSide: 'left', + pathSide: 'start', /** * How text is aligned to the path. This property determines @@ -307,7 +307,6 @@ /** * WARNING: EXPERIMENTAL. NOT SUPPORTED YET * determine the direction of the text. - * This has to be set manually together with textAlign and originX for proper * experience. * some interesting link for the future * https://www.w3.org/International/questions/qa-bidi-unicode-controls @@ -378,6 +377,35 @@ this.setupState({ propertySet: '_dimensionAffectingProps' }); }, + /** + * + * @param {TextAlign} directive + * @param {boolean} rtl + * @returns {TextAlign} + */ + resolveDirectionDirective: function (directive, rtl) { + switch (directive) { + case 'start': + return rtl ? 'right' : 'left'; + case 'end': + return rtl ? 'left' : 'right'; + case 'justify-start': + return rtl ? 'justify-right' : 'justify-left'; + case 'justify-end': + return rtl ? 'justify-left' : 'justify-right'; + default: + return directive; + } + }, + + /** + * @private + * @returns {boolean} + */ + _isPathReversed: function () { + return this.resolveDirectionDirective(this.pathSide, this.direction === 'rtl') === 'right'; + }, + /** * If text has a path, it will add the extra information needed * for path and text calculations @@ -805,7 +833,8 @@ var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length), positionInPath = 0, startingPoint, totalPathLength, path = this.path, - reverse = this.pathSide === 'right'; + reverse = this._isPathReversed(), + textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -828,17 +857,31 @@ startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); startingPoint.x += path.pathOffset.x; startingPoint.y += path.pathOffset.y; - switch (this.textAlign) { + var size = totalPathLength - width; + switch (textAlign) { + case 'start': + case 'justify': + case 'justify-start': + positionInPath = 0; + break; + case 'end': + case 'justify-end': + positionInPath = size; + break; case 'left': - positionInPath = reverse ? (totalPathLength - width) : 0; + case 'justify-left': + positionInPath = reverse ? size : 0; break; case 'center': - positionInPath = (totalPathLength - width) / 2; + case 'justify-center': + positionInPath = size / 2; break; case 'right': - positionInPath = reverse ? 0 : (totalPathLength - width); + case 'justify-right': + positionInPath = reverse ? 0 : size; + break; + default: break; - //todo - add support for justify } positionInPath += this.pathStartOffset * (reverse ? -1 : 1); for (i = reverse ? line.length - 1 : 0; @@ -876,7 +919,7 @@ var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo); graphemeInfo.renderLeft = info.x - startingPoint.x; graphemeInfo.renderTop = info.y - startingPoint.y; - graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); + graphemeInfo.angle = info.angle + (this._isPathReversed() ? Math.PI : 0); }, /** @@ -1295,37 +1338,35 @@ */ _getLineLeftOffset: function(lineIndex) { var lineWidth = this.getLineWidth(lineIndex), - lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, + lineDiff = this.width - lineWidth, textAlign = this.textAlign, rtl = this.direction === 'rtl', isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); - if (textAlign === 'justify' - || (textAlign === 'justify-center' && !isEndOfWrapping) - || (textAlign === 'justify-right' && !isEndOfWrapping) - || (textAlign === 'justify-left' && !isEndOfWrapping) - ) { + if (textAlign === 'justify' || (textAlign.startsWith('justify') && !isEndOfWrapping)) { return 0; } - if (textAlign === 'center') { - leftOffset = lineDiff / 2; - } - if (textAlign === 'right') { - leftOffset = lineDiff; - } - if (textAlign === 'justify-center') { - leftOffset = lineDiff / 2; - } - if (textAlign === 'justify-right') { - leftOffset = lineDiff; - } - if (direction === 'rtl') { - if (textAlign === 'right' || textAlign === 'justify' || textAlign === 'justify-right') { + switch (textAlign) { + case 'start': + case 'justify': + case 'justify-start': leftOffset = 0; - } - else if (textAlign === 'left' || textAlign === 'justify-left') { - leftOffset = -lineDiff; - } - else if (textAlign === 'center' || textAlign === 'justify-center') { - leftOffset = -lineDiff / 2; - } + break; + case 'end': + case 'justify-end': + leftOffset = rtl ? -lineDiff : lineDiff; + break; + case 'left': + case 'justify-left': + leftOffset = rtl ? -lineDiff : 0; + break; + case 'center': + case 'justify-center': + leftOffset = rtl ? -lineDiff / 2 : lineDiff / 2; + break; + case 'right': + case 'justify-right': + leftOffset = rtl ? 0 : lineDiff; + break; + default: + break; } return leftOffset; }, diff --git a/test/unit/itext.js b/test/unit/itext.js index d4c27921a89..daea269e271 100644 --- a/test/unit/itext.js +++ b/test/unit/itext.js @@ -35,7 +35,7 @@ underline: false, overline: false, linethrough: false, - textAlign: 'left', + textAlign: 'start', backgroundColor: '', textBackgroundColor: '', fillRule: 'nonzero', @@ -49,7 +49,7 @@ path: null, direction: 'ltr', pathStartOffset: 0, - pathSide: 'left', + pathSide: 'start', pathAlign: 'baseline' }; @@ -451,44 +451,44 @@ assert.equal(iText.selectLine(0), iText, 'should be chainable'); }); - QUnit.test('findWordBoundaryLeft', function(assert) { + QUnit.test('findWordBoundaryStart', function(assert) { var iText = new fabric.IText('test foo bar-baz\nqux'); - assert.equal(typeof iText.findWordBoundaryLeft, 'function'); + assert.equal(typeof iText.findWordBoundaryStart, 'function'); - assert.equal(iText.findWordBoundaryLeft(3), 0); // 'tes|t' - assert.equal(iText.findWordBoundaryLeft(20), 17); // 'qux|' - assert.equal(iText.findWordBoundaryLeft(6), 5); // 'f|oo' - assert.equal(iText.findWordBoundaryLeft(11), 9); // 'ba|r-baz' + assert.equal(iText.findWordBoundaryStart(3), 0); // 'tes|t' + assert.equal(iText.findWordBoundaryStart(20), 17); // 'qux|' + assert.equal(iText.findWordBoundaryStart(6), 5); // 'f|oo' + assert.equal(iText.findWordBoundaryStart(11), 9); // 'ba|r-baz' }); - QUnit.test('findWordBoundaryRight', function(assert) { + QUnit.test('findWordBoundaryEnd', function(assert) { var iText = new fabric.IText('test foo bar-baz\nqux'); - assert.equal(typeof iText.findWordBoundaryRight, 'function'); + assert.equal(typeof iText.findWordBoundaryEnd, 'function'); - assert.equal(iText.findWordBoundaryRight(3), 4); // 'tes|t' - assert.equal(iText.findWordBoundaryRight(17), 20); // '|qux' - assert.equal(iText.findWordBoundaryRight(6), 8); // 'f|oo' - assert.equal(iText.findWordBoundaryRight(11), 16); // 'ba|r-baz' + assert.equal(iText.findWordBoundaryEnd(3), 4); // 'tes|t' + assert.equal(iText.findWordBoundaryEnd(17), 20); // '|qux' + assert.equal(iText.findWordBoundaryEnd(6), 8); // 'f|oo' + assert.equal(iText.findWordBoundaryEnd(11), 16); // 'ba|r-baz' }); - QUnit.test('findLineBoundaryLeft', function(assert) { + QUnit.test('findLineBoundaryStart', function(assert) { var iText = new fabric.IText('test foo bar-baz\nqux'); - assert.equal(typeof iText.findLineBoundaryLeft, 'function'); + assert.equal(typeof iText.findLineBoundaryStart, 'function'); - assert.equal(iText.findLineBoundaryLeft(3), 0); // 'tes|t' - assert.equal(iText.findLineBoundaryLeft(20), 17); // 'qux|' + assert.equal(iText.findLineBoundaryStart(3), 0); // 'tes|t' + assert.equal(iText.findLineBoundaryStart(20), 17); // 'qux|' }); - QUnit.test('findLineBoundaryRight', function(assert) { + QUnit.test('findLineBoundaryEnd', function(assert) { var iText = new fabric.IText('test foo bar-baz\nqux'); - assert.equal(typeof iText.findLineBoundaryRight, 'function'); + assert.equal(typeof iText.findLineBoundaryEnd, 'function'); - assert.equal(iText.findLineBoundaryRight(3), 16); // 'tes|t' - assert.equal(iText.findLineBoundaryRight(17), 20); // '|qux' + assert.equal(iText.findLineBoundaryEnd(3), 16); // 'tes|t' + assert.equal(iText.findLineBoundaryEnd(17), 20); // '|qux' }); QUnit.test('getSelectionStyles with no arguments', function(assert) { diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 3671b7b4a16..4a6ca9db599 100644 --- a/test/unit/itext_key_behaviour.js +++ b/test/unit/itext_key_behaviour.js @@ -40,16 +40,16 @@ iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorLeft({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorLeft'); + iText.moveCursorBackward({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorBackward'); assert.equal(iText.selectionStart, 1, 'should be 1 less than 2'); assert.equal(iText.selectionEnd, 1, 'should be 1 less than 2'); selection = 0; iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorRight({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorRight'); + iText.moveCursorForward({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorForward'); assert.equal(iText.selectionStart, 3, 'should be 1 more than 2'); assert.equal(iText.selectionEnd, 3, 'should be 1 more than 2'); selection = 0; @@ -80,7 +80,7 @@ iText.selectionStart = 0; iText.selectionEnd = 0; - iText.moveCursorLeft({ shiftKey: false}); + iText.moveCursorBackward({ shiftKey: false}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 0, 'should not move'); @@ -92,7 +92,7 @@ iText.selectionStart = 31; iText.selectionEnd = 31; - iText.moveCursorRight({ shiftKey: false}); + iText.moveCursorForward({ shiftKey: false}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 31, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); @@ -110,21 +110,30 @@ assert.equal(iText.selectionEnd, 9, 'should move to upper line end'); selection = 0; - iText.selectionStart = 1; - iText.selectionEnd = 4; + iText.setSelectionRange(1, 4, 'forward'); + assert.equal(selection, 1, 'should fire after setting selection range'); iText.moveCursorDown({ shiftKey: false }); - assert.equal(selection, 1, 'should fire'); + assert.equal(selection, 2, 'should fire'); assert.equal(iText.selectionStart, 24, 'should move to down line'); assert.equal(iText.selectionEnd, 24, 'should move to down line'); selection = 0; - iText.selectionStart = 28; - iText.selectionEnd = 31; - iText.moveCursorLeft({ shiftKey: false }); - assert.equal(selection, 1, 'should fire'); + iText.setSelectionRange(28, 31, 'backward'); + assert.equal(selection, 1, 'should fire after setting selection range'); + iText.moveCursorBackward({ shiftKey: false }); + assert.equal(selection, 2, 'should fire'); assert.equal(iText.selectionStart, 28, 'should move to selection Start'); assert.equal(iText.selectionEnd, 28, 'should move to selection Start'); selection = 0; + + iText.setSelectionRange(28, 31, 'forward'); + assert.equal(selection, 1, 'should fire after setting selection range'); + iText.moveCursorBackward({ shiftKey: false }); + assert.equal(selection, 2, 'should fire'); + assert.equal(iText.selectionStart, 31, 'should move to selection Start'); + assert.equal(iText.selectionEnd, 31, 'should move to selection Start'); + selection = 0; + // needed or test hangs iText.abortCursorAnimation(); // TODO verify and dp @@ -174,16 +183,16 @@ iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorLeft({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorLeft'); + iText.moveCursorBackward({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorBackward'); assert.equal(iText.selectionStart, 1, 'should be 1 less than 2'); assert.equal(iText.selectionEnd, 1, 'should be 1 less than 2'); selection = 0; iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorRight({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorRight'); + iText.moveCursorForward({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorForward'); assert.equal(iText.selectionStart, 3, 'should be 1 more than 2'); assert.equal(iText.selectionEnd, 3, 'should be 1 more than 2'); selection = 0; @@ -212,29 +221,26 @@ assert.equal(iText.selectionEnd, 0, 'should be back on first line'); selection = 0; - iText.selectionStart = 0; - iText.selectionEnd = 1; - iText._selectionDirection = 'left'; - iText.moveCursorLeft({ shiftKey: true}); - assert.equal(selection, 0, 'should not fire with no change'); + iText.setSelectionRange(0, 1, 'backward'); + assert.equal(selection, 1, 'should fire after setting selection range'); + iText.moveCursorBackward({ shiftKey: true}); + assert.equal(selection, 1, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 1, 'should not move'); iText.moveCursorUp({ shiftKey: true}); - assert.equal(selection, 0, 'should not fire with no change'); + assert.equal(selection, 1, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 1, 'should not move'); selection = 0; - - iText.selectionStart = 30; - iText.selectionEnd = 31; - iText._selectionDirection = 'right'; - iText.moveCursorRight({ shiftKey: true}); - assert.equal(selection, 0, 'should not fire with no change'); + iText.setSelectionRange(30, 31, 'forward'); + assert.equal(selection, 1, 'should fire after setting selection range'); + iText.moveCursorForward({ shiftKey: true}); + assert.equal(selection, 1, 'should not fire with no change'); assert.equal(iText.selectionStart, 30, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); iText.moveCursorDown({ shiftKey: true}); - assert.equal(selection, 0, 'should not fire with no change'); + assert.equal(selection, 1, 'should not fire with no change'); assert.equal(iText.selectionStart, 30, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); selection = 0; diff --git a/test/unit/text.js b/test/unit/text.js index 8df477ff256..d7ad6e34e75 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -43,7 +43,7 @@ underline: false, overline: false, linethrough: false, - textAlign: 'left', + textAlign: 'start', textBackgroundColor: '', fillRule: 'nonzero', paintFirst: 'fill', @@ -56,7 +56,7 @@ strokeUniform: false, direction: 'ltr', pathStartOffset: 0, - pathSide: 'left', + pathSide: 'start', pathAlign: 'baseline' }; @@ -207,7 +207,6 @@ width: 8, height: 18.08, fontSize: 16, - originX: 'left' }); assert.deepEqual(text.toObject(), expectedObject, 'parsed object is what expected'); }); @@ -875,13 +874,23 @@ QUnit.test('_getLineLeftOffset', function(assert) { var text = new fabric.Text('long line of text\nshort'); - assert.equal(text._getLineLeftOffset(1), 0, 'with align left is 0'); + assert.equal(text._getLineLeftOffset(1), 0, 'with align start is 0'); + text.textAlign = 'left'; + assert.equal(text._getLineLeftOffset(1), 0, 'like align start'); text.textAlign = 'right'; assert.equal(Math.round(text._getLineLeftOffset(1)), 174, 'with align right is diff between width and lineWidth'); + text.textAlign = 'end'; + assert.equal(Math.round(text._getLineLeftOffset(1)), 174, 'like align right'); text.textAlign = 'center'; assert.equal(Math.round(text._getLineLeftOffset(1)), 87, 'with align center is split in 2'); text.textAlign = 'justify'; assert.equal(text._getLineLeftOffset(1), 0); + text.textAlign = 'justify-start'; + assert.equal(text._getLineLeftOffset(0), 0, 'is zero for any line but not the last center'); + assert.equal(text._getLineLeftOffset(1), 0); + text.textAlign = 'justify-end'; + assert.equal(text._getLineLeftOffset(0), 0, 'is zero for any line but not the last right'); + assert.equal(Math.round(text._getLineLeftOffset(1)), 174, 'like align right'); text.textAlign = 'justify-center'; assert.equal(text._getLineLeftOffset(0), 0, 'is zero for any line but not the last center'); assert.equal(Math.round(text._getLineLeftOffset(1)), 87, 'like align center'); @@ -896,13 +905,23 @@ QUnit.test('_getLineLeftOffset with direction rtl', function(assert) { var text = new fabric.Text('long line of text\nshort'); text.direction = 'rtl'; - assert.equal(Math.round(text._getLineLeftOffset(1)), -174, 'with align left is diff between width and lineWidth, negative'); + assert.equal(text._getLineLeftOffset(1), 0, 'with align start is 0'); text.textAlign = 'right'; - assert.equal(text._getLineLeftOffset(1), 0, 'with align right is 0'); + assert.equal(text._getLineLeftOffset(1), 0, 'like align start'); + text.textAlign = 'left'; + assert.equal(Math.round(text._getLineLeftOffset(1)), -174, 'with align left is diff between width and lineWidth, negative'); + text.textAlign = 'end'; + assert.equal(Math.round(text._getLineLeftOffset(1)), -174, 'like align left'); text.textAlign = 'center'; assert.equal(Math.round(text._getLineLeftOffset(1)), -87, 'with align center is split in 2'); text.textAlign = 'justify'; assert.equal(text._getLineLeftOffset(1), 0); + text.textAlign = 'justify-start'; + assert.equal(text._getLineLeftOffset(0), 0, 'is zero for any line but not the last right'); + assert.equal(text._getLineLeftOffset(1), 0, 'like align right with rtl'); + text.textAlign = 'justify-end'; + assert.equal(text._getLineLeftOffset(0), 0, 'is zero for any line but not the last left'); + assert.equal(Math.round(text._getLineLeftOffset(1)), -174, 'like align left with rtl'); text.textAlign = 'justify-center'; assert.equal(text._getLineLeftOffset(0), 0, 'is zero for any line but not the last center'); assert.equal(Math.round(text._getLineLeftOffset(1)), -87, 'like align center'); diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 636b48dd5fa..05af638cffa 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -40,7 +40,7 @@ underline: false, overline: false, linethrough: false, - textAlign: 'left', + textAlign: 'start', backgroundColor: '', textBackgroundColor: '', fillRule: 'nonzero', @@ -56,7 +56,7 @@ path: null, direction: 'ltr', pathStartOffset: 0, - pathSide: 'left', + pathSide: 'start', pathAlign: 'baseline' }; @@ -268,7 +268,7 @@ var line2 = textbox._wrapLine('', 0, 100, 0); assert.deepEqual(line2, [[]], 'wrapping with splitByGrapheme'); }); - QUnit.test('texbox will change width from the mr corner', function(assert) { + QUnit.test('textbox will change width from the mr corner', function(assert) { var text = new fabric.Textbox('xa xb xc xd xe ya yb id', { strokeWidth: 0 }); canvas.add(text); canvas.setActiveObject(text); @@ -293,7 +293,7 @@ }); assert.equal(text.width, originalWidth + 20, 'width increased'); }); - QUnit.test('texbox will change width from the ml corner', function(assert) { + QUnit.test('textbox will change width from the ml corner', function(assert) { var text = new fabric.Textbox('xa xb xc xd xe ya yb id', { strokeWidth: 0, left: 40 }); canvas.add(text); canvas.setActiveObject(text);