From e1ceba820cb16f2c26ea6a5c89842ee320dfb689 Mon Sep 17 00:00:00 2001 From: amir hossein Date: Sat, 12 Feb 2022 13:23:07 +0330 Subject: [PATCH 01/61] fix(fabric.Text): support RTL different text alignments --- src/mixins/itext.svg_export.js | 4 ++++ src/mixins/itext_click_behavior.mixin.js | 4 ++-- src/shapes/itext.class.js | 20 ++++++++++++++++++-- src/shapes/text.class.js | 12 ++++++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index 5d8f7a63c25..d31404991ce 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -53,6 +53,7 @@ (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), + (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -75,6 +76,9 @@ // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); + if (this.direction === 'rtl') { + lineOffset += this.width; + } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index eab105f4e88..69b14ed9a13 100644 --- a/src/mixins/itext_click_behavior.mixin.js +++ b/src/mixins/itext_click_behavior.mixin.js @@ -230,7 +230,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = this._getLineLeftOffset(lineIndex); + lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -238,7 +238,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index e7623637e80..7d7f6cd971e 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -366,7 +366,15 @@ left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - boundaries.left *= -1; + if (this.textAlign === 'right' || this.textAlign === 'justify'){ + boundaries.left *= -1; + } + else if (this.textAlign === 'left') { + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + } + else if (this.textAlign === 'center') { + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + } } this.cursorOffsetCache = boundaries; return this.cursorOffsetCache; @@ -455,7 +463,15 @@ ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - drawStart = this.width - drawStart - drawWidth; + if (this.textAlign === 'right' || this.textAlign === 'justify') { + drawStart = this.width - drawStart - drawWidth; + } + else if (this.textAlign === 'left') { + drawStart = boundaries.left + lineOffset - boxEnd; + } + else if (this.textAlign === 'center') { + drawStart = boundaries.left + lineOffset - boxEnd; + } } ctx.fillRect( drawStart, diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 71fc11573e6..f3b7456c594 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1044,7 +1044,7 @@ path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); + drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); @@ -1306,7 +1306,15 @@ leftOffset = lineDiff; } if (direction === 'rtl') { - leftOffset -= lineDiff; + if (textAlign === 'right' || textAlign === 'justify'){ + leftOffset = 0; + } + else if (textAlign === 'left') { + leftOffset = -lineDiff; + } + else if (textAlign === 'center') { + leftOffset = -lineDiff / 2; + } } return leftOffset; }, From 9205aa7275167eb6574b98b242b91e2c1ef075dd Mon Sep 17 00:00:00 2001 From: amir hossein Date: Wed, 16 Feb 2022 09:46:27 +0330 Subject: [PATCH 02/61] fix(fabric.Text): consider justify text alignment in RTL text --- src/shapes/itext.class.js | 12 ++++++------ src/shapes/text.class.js | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 7d7f6cd971e..7e9323ef219 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -366,13 +366,13 @@ left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify'){ + if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { boundaries.left *= -1; } - else if (this.textAlign === 'left') { + else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); } - else if (this.textAlign === 'center') { + else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); } } @@ -463,13 +463,13 @@ ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify') { + if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { drawStart = this.width - drawStart - drawWidth; } - else if (this.textAlign === 'left') { + else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { drawStart = boundaries.left + lineOffset - boxEnd; } - else if (this.textAlign === 'center') { + else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { drawStart = boundaries.left + lineOffset - boxEnd; } } diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index f3b7456c594..1687483236d 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1306,13 +1306,13 @@ leftOffset = lineDiff; } if (direction === 'rtl') { - if (textAlign === 'right' || textAlign === 'justify'){ + if (textAlign === 'right' || textAlign === 'justify' || textAlign === 'justify-right') { leftOffset = 0; } - else if (textAlign === 'left') { + else if (textAlign === 'left' || textAlign === 'justify-left') { leftOffset = -lineDiff; } - else if (textAlign === 'center') { + else if (textAlign === 'center' || textAlign === 'justify-center') { leftOffset = -lineDiff / 2; } } From 18deac646fe2495b23c839fbecd6800e19fb56af Mon Sep 17 00:00:00 2001 From: amir hossein Date: Wed, 16 Feb 2022 10:31:19 +0330 Subject: [PATCH 03/61] fix(fabric.Text): fix justified RTL text issue in export to SVG --- src/mixins/itext.svg_export.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index d31404991ce..326b03c7a53 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -153,7 +153,12 @@ textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - textLeftOffset += boxWidth; + if (this.direction === 'rtl') { + textLeftOffset -= boxWidth; + } + else { + textLeftOffset += boxWidth; + } boxWidth = 0; } } From c8889b30e0a79695f40ad9db0dde3502a113f67e Mon Sep 17 00:00:00 2001 From: Amirhossein Mehrabi Date: Sat, 26 Feb 2022 22:36:46 +0330 Subject: [PATCH 04/61] fix(fabric.Text): add the previous code as comment --- src/shapes/text.class.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 1687483236d..b0cad063771 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1044,6 +1044,8 @@ path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, + // this was changed in the PR #7674 + // currentDirection = ctx.canvas.getAttribute('dir'); drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { From 6b3247c388d220fdb9523fbcbd6a93bd5d5ce41e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 18:56:00 +0200 Subject: [PATCH 05/61] feat(Text): textAlign `start`, `end` and a bit of tidying up --- src/shapes/itext.class.js | 64 ++++++++++++++++++------ src/shapes/text.class.js | 100 +++++++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 7e9323ef219..feb70703d6c 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -366,14 +366,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; @@ -463,14 +479,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 b0cad063771..25e9bbce131 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -124,10 +124,11 @@ /** * Text alignment. Possible values: "left", "center", "right", "justify", * "justify-left", "justify-center" or "justify-right". - * @type String + * @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". @@ -378,6 +379,23 @@ this.setupState({ propertySet: '_dimensionAffectingProps' }); }, + /** + * + * @param {boolean} rtl + * @param {TextAlign} [textAlign] + * @returns {TextAlign} + */ + resolveTextAlign: function (rtl, textAlign) { + switch (textAlign) { + case 'justify-start': + return rtl ? 'justify-right' : 'justify-left'; + case 'justify-end': + return rtl ? 'justify-left' : 'justify-right'; + default: + return textAlign; + } + }, + /** * If text has a path, it will add the extra information needed * for path and text calculations @@ -805,7 +823,7 @@ 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.pathSide === 'right', textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -828,17 +846,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; @@ -1286,37 +1318,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; }, From 67162a4e7d39266eb8ccfbfa2d34af814227eb77 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:26:21 +0200 Subject: [PATCH 06/61] cleanup --- src/mixins/itext_behavior.mixin.js | 24 ++++ src/mixins/itext_key_behavior.mixin.js | 169 ++++++++++--------------- 2 files changed, 92 insertions(+), 101 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 4d075b4f4a4..2a51e3c96ff 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -188,6 +188,18 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findWordBoundary: function (from, startFrom) { + return from === 'right' ? + this.findWordBoundaryRight(startFrom) : + this.findWordBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index @@ -234,6 +246,18 @@ return startFrom + offset; }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findLineBoundary: function (from, startFrom) { + return from === 'right' ? + this.findLineBoundaryRight(startFrom) : + this.findLineBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index d45217fd7c2..00a7369fb76 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -501,104 +501,67 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 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 = true; + if (e.shiftKey) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + changed = this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0) { + this._selectionDirection = 'left'; + changed = this._moveLeft(e, 'selectionStart'); + } } 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'); + + // 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) { + changed = this._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; } + this._invalidateCursor(changed); }, /** * Moves cursor right * @param {Event} e Event object */ - moveCursorRight: function(e) { + moveCursorRight: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - this._moveCursorLeftOrRight('Right', e); + var changed = true; + if (e.shiftKey) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + changed = this._moveRight(e, 'selectionEnd'); + } + } + else { + this._selectionDirection = 'right'; + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + } + 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(); @@ -607,35 +570,39 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor right while keeping selection - * @param {Event} e + * @private + * @return {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; + if (e.altKey) { + newValue = this.findWordBoundary(direction, this[prop]); } - else if (this.selectionEnd !== this._text.length) { - this._selectionDirection = 'right'; - return this._moveRight(e, 'selectionEnd'); + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this.findLineBoundary(direction, this[prop]); + } + else { + this[prop] += direction === 'left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; } }, /** - * Moves cursor right without keeping selection - * @param {Event} e Event object + * @private */ - moveCursorRightWithoutShift: function(e) { - var changed = true; - this._selectionDirection = 'right'; + _moveLeft: function(e, prop) { + return this._move(e, prop, 'left'); + }, - if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); - this.selectionEnd = this.selectionStart; - } - else { - this.selectionStart = this.selectionEnd; - } - return changed; + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'right'); }, /** From 40bddfa7fdb784427fba636497c808c5937566c4 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:40:40 +0200 Subject: [PATCH 07/61] lint + rename --- src/mixins/itext_behavior.mixin.js | 16 ++++++++-------- src/mixins/itext_key_behavior.mixin.js | 22 +++++++++++----------- src/shapes/text.class.js | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 2a51e3c96ff..f50d1f67246 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -189,15 +189,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findWordBoundary: function (from, startFrom) { return from === 'right' ? this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom) + this.findWordBoundaryLeft(startFrom); }, /** @@ -247,15 +247,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findLineBoundary: function (from, startFrom) { return from === 'right' ? this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom) + this.findLineBoundaryLeft(startFrom); }, /** diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 00a7369fb76..fdc907ad82e 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -57,11 +57,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: 'moveCursorEndDir', + 36: 'moveCursorStartDir', + 37: 'moveCursorStartDir', 38: 'moveCursorUp', - 39: 'moveCursorRight', + 39: 'moveCursorEndDir', 40: 'moveCursorDown', }, @@ -70,11 +70,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: 'moveCursorStartDir', + 36: 'moveCursorEndDir', + 37: 'moveCursorEndDir', 38: 'moveCursorUp', - 39: 'moveCursorLeft', + 39: 'moveCursorStartDir', 40: 'moveCursorDown', }, @@ -497,7 +497,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor left * @param {Event} e Event object */ - moveCursorLeft: function(e) { + moveCursorStartDir: function (e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } @@ -528,7 +528,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor right * @param {Event} e Event object */ - moveCursorRight: function (e) { + moveCursorEndDir: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } @@ -557,7 +557,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private - * @param {boolean} dirty + * @param {boolean} dirty */ _invalidateCursor: function (dirty) { this._currentCursorOpacity = 1; diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 25e9bbce131..d7ce04b1f8d 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -380,8 +380,8 @@ }, /** - * - * @param {boolean} rtl + * + * @param {boolean} rtl * @param {TextAlign} [textAlign] * @returns {TextAlign} */ From ac8cbfba772941ce1142e503f34b9d717082aef0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:05:57 +0200 Subject: [PATCH 08/61] pathSide --- src/shapes/text.class.js | 32 ++++++++++++++++++++++---------- test/unit/itext.js | 4 ++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index d7ce04b1f8d..f2f095ca4b8 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -122,8 +122,7 @@ linethrough: false, /** - * Text alignment. Possible values: "left", "center", "right", "justify", - * "justify-left", "justify-center" or "justify-right". + * Text alignment. * @typedef {'start' | 'center' | 'end' | 'left' | 'right' | 'justify' | 'justify-start' | 'justify-center' | 'justify-end' | 'justify-left' | 'justify-right'} TextAlign * @type {TextAlign} * @default @@ -236,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 @@ -381,21 +380,33 @@ /** * + * @param {TextAlign} directive * @param {boolean} rtl - * @param {TextAlign} [textAlign] * @returns {TextAlign} */ - resolveTextAlign: function (rtl, textAlign) { - switch (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 textAlign; + 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 @@ -823,7 +834,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', textAlign = this.textAlign; + reverse = this._isPathReversed(), + textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -908,7 +920,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); }, /** diff --git a/test/unit/itext.js b/test/unit/itext.js index 72d65cfda41..5074cbab639 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' }; From 72828f76bc8e9c22bda3ddc5c48fb3f0f20bc0a6 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:17:45 +0200 Subject: [PATCH 09/61] fix(tests) --- src/mixins/itext_key_behavior.mixin.js | 7 ++++--- test/unit/itext_key_behaviour.js | 26 +++++++++++++------------- test/unit/text.js | 4 ++-- test/unit/textbox.js | 4 ++-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index fdc907ad82e..68d9371794b 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -501,7 +501,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } - var changed = true; + var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { changed = this._moveLeft(e, 'selectionEnd'); @@ -512,8 +512,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } else { + 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 && this.selectionStart !== 0) { @@ -532,7 +532,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - var changed = true; + var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { changed = this._moveRight(e, 'selectionStart'); @@ -543,6 +543,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } else { + changed = true; this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { changed = this._moveRight(e, 'selectionStart'); diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 3671b7b4a16..785696abd2b 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.moveCursorStartDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); 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.moveCursorEndDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); 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.moveCursorStartDir({ 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.moveCursorEndDir({ 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'); @@ -120,7 +120,7 @@ iText.selectionStart = 28; iText.selectionEnd = 31; - iText.moveCursorLeft({ shiftKey: false }); + iText.moveCursorStartDir({ shiftKey: false }); assert.equal(selection, 1, 'should fire'); assert.equal(iText.selectionStart, 28, 'should move to selection Start'); assert.equal(iText.selectionEnd, 28, 'should move to selection Start'); @@ -174,16 +174,16 @@ iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorLeft({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorLeft'); + iText.moveCursorStartDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); 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.moveCursorEndDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); assert.equal(iText.selectionStart, 3, 'should be 1 more than 2'); assert.equal(iText.selectionEnd, 3, 'should be 1 more than 2'); selection = 0; @@ -215,7 +215,7 @@ iText.selectionStart = 0; iText.selectionEnd = 1; iText._selectionDirection = 'left'; - iText.moveCursorLeft({ shiftKey: true}); + iText.moveCursorStartDir({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 1, 'should not move'); @@ -229,7 +229,7 @@ iText.selectionStart = 30; iText.selectionEnd = 31; iText._selectionDirection = 'right'; - iText.moveCursorRight({ shiftKey: true}); + iText.moveCursorEndDir({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 30, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); diff --git a/test/unit/text.js b/test/unit/text.js index cbd956e5cd6..8f2546e8778 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' }; diff --git a/test/unit/textbox.js b/test/unit/textbox.js index e47522b19b7..54aa82b0e20 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' }; From 41f033a9b74e5ac1f6d6e30df632c80f58779885 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:28:48 +0200 Subject: [PATCH 10/61] Update text.js --- test/unit/text.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/unit/text.js b/test/unit/text.js index 8f2546e8778..fff00778a27 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -875,13 +875,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 +906,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'); From ebff078043708989cff1152c2cd1978ced3ac6f5 Mon Sep 17 00:00:00 2001 From: amir hossein Date: Sat, 12 Feb 2022 13:23:07 +0330 Subject: [PATCH 11/61] fix(fabric.Text): support RTL different text alignments --- src/mixins/itext.svg_export.js | 4 ++++ src/mixins/itext_click_behavior.mixin.js | 4 ++-- src/shapes/itext.class.js | 20 ++++++++++++++++++-- src/shapes/text.class.js | 12 ++++++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index 5d8f7a63c25..d31404991ce 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -53,6 +53,7 @@ (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), + (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -75,6 +76,9 @@ // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); + if (this.direction === 'rtl') { + lineOffset += this.width; + } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index eab105f4e88..69b14ed9a13 100644 --- a/src/mixins/itext_click_behavior.mixin.js +++ b/src/mixins/itext_click_behavior.mixin.js @@ -230,7 +230,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = this._getLineLeftOffset(lineIndex); + lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -238,7 +238,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index b4e66b3ad63..1a4df81864b 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -356,7 +356,15 @@ left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - boundaries.left *= -1; + if (this.textAlign === 'right' || this.textAlign === 'justify'){ + boundaries.left *= -1; + } + else if (this.textAlign === 'left') { + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + } + else if (this.textAlign === 'center') { + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + } } this.cursorOffsetCache = boundaries; return this.cursorOffsetCache; @@ -445,7 +453,15 @@ ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - drawStart = this.width - drawStart - drawWidth; + if (this.textAlign === 'right' || this.textAlign === 'justify') { + drawStart = this.width - drawStart - drawWidth; + } + else if (this.textAlign === 'left') { + drawStart = boundaries.left + lineOffset - boxEnd; + } + else if (this.textAlign === 'center') { + drawStart = boundaries.left + lineOffset - boxEnd; + } } ctx.fillRect( drawStart, diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 2bb6bc2e5f7..25c9b5b05c0 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1044,7 +1044,7 @@ path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); + drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); @@ -1306,7 +1306,15 @@ leftOffset = lineDiff; } if (direction === 'rtl') { - leftOffset -= lineDiff; + if (textAlign === 'right' || textAlign === 'justify'){ + leftOffset = 0; + } + else if (textAlign === 'left') { + leftOffset = -lineDiff; + } + else if (textAlign === 'center') { + leftOffset = -lineDiff / 2; + } } return leftOffset; }, From cda1fc326a1ff9182678d81527fd0d290f136cc3 Mon Sep 17 00:00:00 2001 From: amir hossein Date: Wed, 16 Feb 2022 09:46:27 +0330 Subject: [PATCH 12/61] fix(fabric.Text): consider justify text alignment in RTL text --- src/shapes/itext.class.js | 12 ++++++------ src/shapes/text.class.js | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 1a4df81864b..b9228c24e8f 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -356,13 +356,13 @@ left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify'){ + if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { boundaries.left *= -1; } - else if (this.textAlign === 'left') { + else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); } - else if (this.textAlign === 'center') { + else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); } } @@ -453,13 +453,13 @@ ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify') { + if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { drawStart = this.width - drawStart - drawWidth; } - else if (this.textAlign === 'left') { + else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { drawStart = boundaries.left + lineOffset - boxEnd; } - else if (this.textAlign === 'center') { + else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { drawStart = boundaries.left + lineOffset - boxEnd; } } diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 25c9b5b05c0..525841935bb 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1306,13 +1306,13 @@ leftOffset = lineDiff; } if (direction === 'rtl') { - if (textAlign === 'right' || textAlign === 'justify'){ + if (textAlign === 'right' || textAlign === 'justify' || textAlign === 'justify-right') { leftOffset = 0; } - else if (textAlign === 'left') { + else if (textAlign === 'left' || textAlign === 'justify-left') { leftOffset = -lineDiff; } - else if (textAlign === 'center') { + else if (textAlign === 'center' || textAlign === 'justify-center') { leftOffset = -lineDiff / 2; } } From 0b9215b6b83cd47b5a7f09e622d218ab5fc04dfa Mon Sep 17 00:00:00 2001 From: amir hossein Date: Wed, 16 Feb 2022 10:31:19 +0330 Subject: [PATCH 13/61] fix(fabric.Text): fix justified RTL text issue in export to SVG --- src/mixins/itext.svg_export.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index d31404991ce..326b03c7a53 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -153,7 +153,12 @@ textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - textLeftOffset += boxWidth; + if (this.direction === 'rtl') { + textLeftOffset -= boxWidth; + } + else { + textLeftOffset += boxWidth; + } boxWidth = 0; } } From 8a8a1e60b9c061989c9b55e2a538c78a07c1f5fb Mon Sep 17 00:00:00 2001 From: Amirhossein Mehrabi Date: Sat, 26 Feb 2022 22:36:46 +0330 Subject: [PATCH 14/61] fix(fabric.Text): add the previous code as comment --- src/shapes/text.class.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 525841935bb..0c94fe81738 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1044,6 +1044,8 @@ path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, + // this was changed in the PR #7674 + // currentDirection = ctx.canvas.getAttribute('dir'); drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { From 9ed6904dfba52d6a1931ca3f574ca98b4d5c5bf5 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 18:56:00 +0200 Subject: [PATCH 15/61] feat(Text): textAlign `start`, `end` and a bit of tidying up --- src/shapes/itext.class.js | 64 ++++++++++++++++++------ src/shapes/text.class.js | 100 +++++++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index b9228c24e8f..351b2416c7e 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -356,14 +356,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; @@ -453,14 +469,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 0c94fe81738..46461e600c8 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -124,10 +124,11 @@ /** * Text alignment. Possible values: "left", "center", "right", "justify", * "justify-left", "justify-center" or "justify-right". - * @type String + * @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". @@ -378,6 +379,23 @@ this.setupState({ propertySet: '_dimensionAffectingProps' }); }, + /** + * + * @param {boolean} rtl + * @param {TextAlign} [textAlign] + * @returns {TextAlign} + */ + resolveTextAlign: function (rtl, textAlign) { + switch (textAlign) { + case 'justify-start': + return rtl ? 'justify-right' : 'justify-left'; + case 'justify-end': + return rtl ? 'justify-left' : 'justify-right'; + default: + return textAlign; + } + }, + /** * If text has a path, it will add the extra information needed * for path and text calculations @@ -805,7 +823,7 @@ 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.pathSide === 'right', textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -828,17 +846,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; @@ -1286,37 +1318,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; }, From 4281e59823ec577ec809c50ce540345f1448ad6f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:26:21 +0200 Subject: [PATCH 16/61] cleanup --- src/mixins/itext_behavior.mixin.js | 24 ++++ src/mixins/itext_key_behavior.mixin.js | 169 ++++++++++--------------- 2 files changed, 92 insertions(+), 101 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 4d075b4f4a4..2a51e3c96ff 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -188,6 +188,18 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findWordBoundary: function (from, startFrom) { + return from === 'right' ? + this.findWordBoundaryRight(startFrom) : + this.findWordBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index @@ -234,6 +246,18 @@ return startFrom + offset; }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findLineBoundary: function (from, startFrom) { + return from === 'right' ? + this.findLineBoundaryRight(startFrom) : + this.findLineBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index d45217fd7c2..00a7369fb76 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -501,104 +501,67 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 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 = true; + if (e.shiftKey) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + changed = this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0) { + this._selectionDirection = 'left'; + changed = this._moveLeft(e, 'selectionStart'); + } } 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'); + + // 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) { + changed = this._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; } + this._invalidateCursor(changed); }, /** * Moves cursor right * @param {Event} e Event object */ - moveCursorRight: function(e) { + moveCursorRight: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - this._moveCursorLeftOrRight('Right', e); + var changed = true; + if (e.shiftKey) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + changed = this._moveRight(e, 'selectionEnd'); + } + } + else { + this._selectionDirection = 'right'; + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + } + 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(); @@ -607,35 +570,39 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor right while keeping selection - * @param {Event} e + * @private + * @return {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; + if (e.altKey) { + newValue = this.findWordBoundary(direction, this[prop]); } - else if (this.selectionEnd !== this._text.length) { - this._selectionDirection = 'right'; - return this._moveRight(e, 'selectionEnd'); + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this.findLineBoundary(direction, this[prop]); + } + else { + this[prop] += direction === 'left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; } }, /** - * Moves cursor right without keeping selection - * @param {Event} e Event object + * @private */ - moveCursorRightWithoutShift: function(e) { - var changed = true; - this._selectionDirection = 'right'; + _moveLeft: function(e, prop) { + return this._move(e, prop, 'left'); + }, - if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); - this.selectionEnd = this.selectionStart; - } - else { - this.selectionStart = this.selectionEnd; - } - return changed; + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'right'); }, /** From ff0f86540ae111dec2c77b7c676a56d771e7e204 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:40:40 +0200 Subject: [PATCH 17/61] lint + rename --- src/mixins/itext_behavior.mixin.js | 16 ++++++++-------- src/mixins/itext_key_behavior.mixin.js | 22 +++++++++++----------- src/shapes/text.class.js | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 2a51e3c96ff..f50d1f67246 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -189,15 +189,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findWordBoundary: function (from, startFrom) { return from === 'right' ? this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom) + this.findWordBoundaryLeft(startFrom); }, /** @@ -247,15 +247,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findLineBoundary: function (from, startFrom) { return from === 'right' ? this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom) + this.findLineBoundaryLeft(startFrom); }, /** diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 00a7369fb76..fdc907ad82e 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -57,11 +57,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: 'moveCursorEndDir', + 36: 'moveCursorStartDir', + 37: 'moveCursorStartDir', 38: 'moveCursorUp', - 39: 'moveCursorRight', + 39: 'moveCursorEndDir', 40: 'moveCursorDown', }, @@ -70,11 +70,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: 'moveCursorStartDir', + 36: 'moveCursorEndDir', + 37: 'moveCursorEndDir', 38: 'moveCursorUp', - 39: 'moveCursorLeft', + 39: 'moveCursorStartDir', 40: 'moveCursorDown', }, @@ -497,7 +497,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor left * @param {Event} e Event object */ - moveCursorLeft: function(e) { + moveCursorStartDir: function (e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } @@ -528,7 +528,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor right * @param {Event} e Event object */ - moveCursorRight: function (e) { + moveCursorEndDir: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } @@ -557,7 +557,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private - * @param {boolean} dirty + * @param {boolean} dirty */ _invalidateCursor: function (dirty) { this._currentCursorOpacity = 1; diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 46461e600c8..c2f5646432d 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -380,8 +380,8 @@ }, /** - * - * @param {boolean} rtl + * + * @param {boolean} rtl * @param {TextAlign} [textAlign] * @returns {TextAlign} */ From 689749e20a4ab1bbffab8d287b08511ea0904e1d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:05:57 +0200 Subject: [PATCH 18/61] pathSide --- src/shapes/text.class.js | 32 ++++++++++++++++++++++---------- test/unit/itext.js | 4 ++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index c2f5646432d..79b2abece9b 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -122,8 +122,7 @@ linethrough: false, /** - * Text alignment. Possible values: "left", "center", "right", "justify", - * "justify-left", "justify-center" or "justify-right". + * Text alignment. * @typedef {'start' | 'center' | 'end' | 'left' | 'right' | 'justify' | 'justify-start' | 'justify-center' | 'justify-end' | 'justify-left' | 'justify-right'} TextAlign * @type {TextAlign} * @default @@ -236,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 @@ -381,21 +380,33 @@ /** * + * @param {TextAlign} directive * @param {boolean} rtl - * @param {TextAlign} [textAlign] * @returns {TextAlign} */ - resolveTextAlign: function (rtl, textAlign) { - switch (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 textAlign; + 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 @@ -823,7 +834,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', textAlign = this.textAlign; + reverse = this._isPathReversed(), + textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -908,7 +920,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); }, /** diff --git a/test/unit/itext.js b/test/unit/itext.js index 3bb0704e8de..6a42fb54415 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' }; From 89582310b5cdff236753db34f60b0a5bf8ca11e6 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:17:45 +0200 Subject: [PATCH 19/61] fix(tests) --- src/mixins/itext_key_behavior.mixin.js | 7 ++++--- test/unit/itext_key_behaviour.js | 26 +++++++++++++------------- test/unit/text.js | 4 ++-- test/unit/textbox.js | 4 ++-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index fdc907ad82e..68d9371794b 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -501,7 +501,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } - var changed = true; + var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { changed = this._moveLeft(e, 'selectionEnd'); @@ -512,8 +512,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } else { + 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 && this.selectionStart !== 0) { @@ -532,7 +532,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - var changed = true; + var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { changed = this._moveRight(e, 'selectionStart'); @@ -543,6 +543,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } else { + changed = true; this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { changed = this._moveRight(e, 'selectionStart'); diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 3671b7b4a16..785696abd2b 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.moveCursorStartDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); 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.moveCursorEndDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); 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.moveCursorStartDir({ 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.moveCursorEndDir({ 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'); @@ -120,7 +120,7 @@ iText.selectionStart = 28; iText.selectionEnd = 31; - iText.moveCursorLeft({ shiftKey: false }); + iText.moveCursorStartDir({ shiftKey: false }); assert.equal(selection, 1, 'should fire'); assert.equal(iText.selectionStart, 28, 'should move to selection Start'); assert.equal(iText.selectionEnd, 28, 'should move to selection Start'); @@ -174,16 +174,16 @@ iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorLeft({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorLeft'); + iText.moveCursorStartDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); 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.moveCursorEndDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); assert.equal(iText.selectionStart, 3, 'should be 1 more than 2'); assert.equal(iText.selectionEnd, 3, 'should be 1 more than 2'); selection = 0; @@ -215,7 +215,7 @@ iText.selectionStart = 0; iText.selectionEnd = 1; iText._selectionDirection = 'left'; - iText.moveCursorLeft({ shiftKey: true}); + iText.moveCursorStartDir({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 1, 'should not move'); @@ -229,7 +229,7 @@ iText.selectionStart = 30; iText.selectionEnd = 31; iText._selectionDirection = 'right'; - iText.moveCursorRight({ shiftKey: true}); + iText.moveCursorEndDir({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 30, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); diff --git a/test/unit/text.js b/test/unit/text.js index 8df477ff256..35fb395a9c9 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' }; diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 90f8ff75461..4c3a2009d92 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' }; From c7736fa03ccd90b90bc8a1981cbb068358707ab6 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:32:51 +0200 Subject: [PATCH 20/61] Update text.js --- test/unit/text.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/unit/text.js b/test/unit/text.js index 35fb395a9c9..3d9615f8cba 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -875,13 +875,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 +906,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'); From 1ea246502f12775a0de0db13115c35d93baa71e7 Mon Sep 17 00:00:00 2001 From: Amirhossein Mehrabi Date: Sat, 26 Feb 2022 22:36:46 +0330 Subject: [PATCH 21/61] fix(fabric.Text): add the previous code as comment --- src/shapes/text.class.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 2bb6bc2e5f7..76ed534c3c0 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1044,7 +1044,9 @@ path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); + // this was changed in the PR #7674 + // currentDirection = ctx.canvas.getAttribute('dir'); + drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); From 2e608dcdde612cbc63f50354a7ea9df7a9dcfd6c Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Wed, 9 Mar 2022 09:37:43 +0200 Subject: [PATCH 22/61] feat(): dataURL export - filter objects (#7788) * feat(): filter options * visual test From 76f94b4ec678105609a8fd53c6b85ad7e6d8de23 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 18:56:00 +0200 Subject: [PATCH 23/61] feat(Text): textAlign `start`, `end` and a bit of tidying up --- src/shapes/itext.class.js | 52 +++++++++++++++++++++- src/shapes/text.class.js | 94 +++++++++++++++++++++++++++------------ 2 files changed, 116 insertions(+), 30 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index b4e66b3ad63..351b2416c7e 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -356,7 +356,31 @@ left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - boundaries.left *= -1; + 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; return this.cursorOffsetCache; @@ -445,7 +469,31 @@ ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - drawStart = this.width - drawStart - drawWidth; + 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( drawStart, diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 76ed534c3c0..46461e600c8 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -124,10 +124,11 @@ /** * Text alignment. Possible values: "left", "center", "right", "justify", * "justify-left", "justify-center" or "justify-right". - * @type String + * @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". @@ -378,6 +379,23 @@ this.setupState({ propertySet: '_dimensionAffectingProps' }); }, + /** + * + * @param {boolean} rtl + * @param {TextAlign} [textAlign] + * @returns {TextAlign} + */ + resolveTextAlign: function (rtl, textAlign) { + switch (textAlign) { + case 'justify-start': + return rtl ? 'justify-right' : 'justify-left'; + case 'justify-end': + return rtl ? 'justify-left' : 'justify-right'; + default: + return textAlign; + } + }, + /** * If text has a path, it will add the extra information needed * for path and text calculations @@ -805,7 +823,7 @@ 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.pathSide === 'right', textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -828,17 +846,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; @@ -1286,29 +1318,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') { - leftOffset -= lineDiff; + switch (textAlign) { + case 'start': + case 'justify': + case 'justify-start': + leftOffset = 0; + 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; }, From 3b80c7bdfd78ae58ca387e370fc675441c8af9e4 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:26:21 +0200 Subject: [PATCH 24/61] cleanup --- src/mixins/itext_behavior.mixin.js | 24 ++++ src/mixins/itext_key_behavior.mixin.js | 169 ++++++++++--------------- 2 files changed, 92 insertions(+), 101 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 4d075b4f4a4..2a51e3c96ff 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -188,6 +188,18 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findWordBoundary: function (from, startFrom) { + return from === 'right' ? + this.findWordBoundaryRight(startFrom) : + this.findWordBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index @@ -234,6 +246,18 @@ return startFrom + offset; }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findLineBoundary: function (from, startFrom) { + return from === 'right' ? + this.findLineBoundaryRight(startFrom) : + this.findLineBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index d45217fd7c2..00a7369fb76 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -501,104 +501,67 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 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 = true; + if (e.shiftKey) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + changed = this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0) { + this._selectionDirection = 'left'; + changed = this._moveLeft(e, 'selectionStart'); + } } 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'); + + // 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) { + changed = this._moveLeft(e, 'selectionStart'); + } + this.selectionEnd = this.selectionStart; } + this._invalidateCursor(changed); }, /** * Moves cursor right * @param {Event} e Event object */ - moveCursorRight: function(e) { + moveCursorRight: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - this._moveCursorLeftOrRight('Right', e); + var changed = true; + if (e.shiftKey) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + changed = this._moveRight(e, 'selectionEnd'); + } + } + else { + this._selectionDirection = 'right'; + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + } + 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(); @@ -607,35 +570,39 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor right while keeping selection - * @param {Event} e + * @private + * @return {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; + if (e.altKey) { + newValue = this.findWordBoundary(direction, this[prop]); } - else if (this.selectionEnd !== this._text.length) { - this._selectionDirection = 'right'; - return this._moveRight(e, 'selectionEnd'); + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this.findLineBoundary(direction, this[prop]); + } + else { + this[prop] += direction === 'left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; } }, /** - * Moves cursor right without keeping selection - * @param {Event} e Event object + * @private */ - moveCursorRightWithoutShift: function(e) { - var changed = true; - this._selectionDirection = 'right'; + _moveLeft: function(e, prop) { + return this._move(e, prop, 'left'); + }, - if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); - this.selectionEnd = this.selectionStart; - } - else { - this.selectionStart = this.selectionEnd; - } - return changed; + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'right'); }, /** From 1a3f138f6bb11e7d9bd335763b708490d77ec183 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:40:40 +0200 Subject: [PATCH 25/61] lint + rename --- src/mixins/itext_behavior.mixin.js | 16 ++++++++-------- src/mixins/itext_key_behavior.mixin.js | 22 +++++++++++----------- src/shapes/text.class.js | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 2a51e3c96ff..f50d1f67246 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -189,15 +189,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findWordBoundary: function (from, startFrom) { return from === 'right' ? this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom) + this.findWordBoundaryLeft(startFrom); }, /** @@ -247,15 +247,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findLineBoundary: function (from, startFrom) { return from === 'right' ? this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom) + this.findLineBoundaryLeft(startFrom); }, /** diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 00a7369fb76..fdc907ad82e 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -57,11 +57,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: 'moveCursorEndDir', + 36: 'moveCursorStartDir', + 37: 'moveCursorStartDir', 38: 'moveCursorUp', - 39: 'moveCursorRight', + 39: 'moveCursorEndDir', 40: 'moveCursorDown', }, @@ -70,11 +70,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: 'moveCursorStartDir', + 36: 'moveCursorEndDir', + 37: 'moveCursorEndDir', 38: 'moveCursorUp', - 39: 'moveCursorLeft', + 39: 'moveCursorStartDir', 40: 'moveCursorDown', }, @@ -497,7 +497,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor left * @param {Event} e Event object */ - moveCursorLeft: function(e) { + moveCursorStartDir: function (e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } @@ -528,7 +528,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor right * @param {Event} e Event object */ - moveCursorRight: function (e) { + moveCursorEndDir: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } @@ -557,7 +557,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private - * @param {boolean} dirty + * @param {boolean} dirty */ _invalidateCursor: function (dirty) { this._currentCursorOpacity = 1; diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 46461e600c8..c2f5646432d 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -380,8 +380,8 @@ }, /** - * - * @param {boolean} rtl + * + * @param {boolean} rtl * @param {TextAlign} [textAlign] * @returns {TextAlign} */ From 681c0198fa4861c24b44cf21768f7ecf3b328ade Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:05:57 +0200 Subject: [PATCH 26/61] pathSide --- src/shapes/text.class.js | 32 ++++++++++++++++++++++---------- test/unit/itext.js | 4 ++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index c2f5646432d..79b2abece9b 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -122,8 +122,7 @@ linethrough: false, /** - * Text alignment. Possible values: "left", "center", "right", "justify", - * "justify-left", "justify-center" or "justify-right". + * Text alignment. * @typedef {'start' | 'center' | 'end' | 'left' | 'right' | 'justify' | 'justify-start' | 'justify-center' | 'justify-end' | 'justify-left' | 'justify-right'} TextAlign * @type {TextAlign} * @default @@ -236,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 @@ -381,21 +380,33 @@ /** * + * @param {TextAlign} directive * @param {boolean} rtl - * @param {TextAlign} [textAlign] * @returns {TextAlign} */ - resolveTextAlign: function (rtl, textAlign) { - switch (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 textAlign; + 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 @@ -823,7 +834,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', textAlign = this.textAlign; + reverse = this._isPathReversed(), + textAlign = this.textAlign; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -908,7 +920,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); }, /** diff --git a/test/unit/itext.js b/test/unit/itext.js index 3bb0704e8de..6a42fb54415 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' }; From 28596f5b08c3620deadc5fd3d56d66d1ce68ca68 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:17:45 +0200 Subject: [PATCH 27/61] fix(tests) --- src/mixins/itext_key_behavior.mixin.js | 7 ++++--- test/unit/itext_key_behaviour.js | 26 +++++++++++++------------- test/unit/text.js | 4 ++-- test/unit/textbox.js | 4 ++-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index fdc907ad82e..68d9371794b 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -501,7 +501,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } - var changed = true; + var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { changed = this._moveLeft(e, 'selectionEnd'); @@ -512,8 +512,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } else { + 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 && this.selectionStart !== 0) { @@ -532,7 +532,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - var changed = true; + var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { changed = this._moveRight(e, 'selectionStart'); @@ -543,6 +543,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } else { + changed = true; this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { changed = this._moveRight(e, 'selectionStart'); diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 3671b7b4a16..785696abd2b 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.moveCursorStartDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); 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.moveCursorEndDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); 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.moveCursorStartDir({ 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.moveCursorEndDir({ 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'); @@ -120,7 +120,7 @@ iText.selectionStart = 28; iText.selectionEnd = 31; - iText.moveCursorLeft({ shiftKey: false }); + iText.moveCursorStartDir({ shiftKey: false }); assert.equal(selection, 1, 'should fire'); assert.equal(iText.selectionStart, 28, 'should move to selection Start'); assert.equal(iText.selectionEnd, 28, 'should move to selection Start'); @@ -174,16 +174,16 @@ iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorLeft({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorLeft'); + iText.moveCursorStartDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); 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.moveCursorEndDir({ shiftKey: false}); + assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); assert.equal(iText.selectionStart, 3, 'should be 1 more than 2'); assert.equal(iText.selectionEnd, 3, 'should be 1 more than 2'); selection = 0; @@ -215,7 +215,7 @@ iText.selectionStart = 0; iText.selectionEnd = 1; iText._selectionDirection = 'left'; - iText.moveCursorLeft({ shiftKey: true}); + iText.moveCursorStartDir({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 1, 'should not move'); @@ -229,7 +229,7 @@ iText.selectionStart = 30; iText.selectionEnd = 31; iText._selectionDirection = 'right'; - iText.moveCursorRight({ shiftKey: true}); + iText.moveCursorEndDir({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 30, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); diff --git a/test/unit/text.js b/test/unit/text.js index 8df477ff256..35fb395a9c9 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' }; diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 90f8ff75461..4c3a2009d92 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' }; From 07f597d49db11bb0b8df70e883cd3d3eca8652ed Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:28:48 +0200 Subject: [PATCH 28/61] Update text.js --- test/unit/text.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/unit/text.js b/test/unit/text.js index 35fb395a9c9..3d9615f8cba 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -875,13 +875,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 +906,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'); From fb3054b5475c23bc4ca8f52de3140b378838647b Mon Sep 17 00:00:00 2001 From: amir hossein Date: Sat, 12 Feb 2022 13:23:07 +0330 Subject: [PATCH 29/61] fix(fabric.Text): support RTL different text alignments --- src/mixins/itext.svg_export.js | 4 ++++ src/mixins/itext_click_behavior.mixin.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index 5d8f7a63c25..d31404991ce 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -53,6 +53,7 @@ (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), + (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -75,6 +76,9 @@ // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); + if (this.direction === 'rtl') { + lineOffset += this.width; + } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index eab105f4e88..69b14ed9a13 100644 --- a/src/mixins/itext_click_behavior.mixin.js +++ b/src/mixins/itext_click_behavior.mixin.js @@ -230,7 +230,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = this._getLineLeftOffset(lineIndex); + lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -238,7 +238,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; From 4f9529e2e73a3544cc42523c1daa18f87696c2a4 Mon Sep 17 00:00:00 2001 From: amir hossein Date: Wed, 16 Feb 2022 09:46:27 +0330 Subject: [PATCH 30/61] fix(fabric.Text): consider justify text alignment in RTL text From c021097db4d9a5ba64b716d6f3854fdc0a7af3b7 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 09:03:07 +0200 Subject: [PATCH 31/61] feat(IText): selectionDirection, selectionDirection --- src/mixins/itext_behavior.mixin.js | 17 +++++++++++---- src/shapes/itext.class.js | 35 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index f50d1f67246..ea15ad69ec9 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -472,11 +472,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.__selectionStartOnMouseDown ? 'backward' : 'forward' + }; }, /** @@ -489,8 +493,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(); }, @@ -514,6 +522,7 @@ if (!this.inCompositionMode) { this.selectionStart = newSelection.selectionStart; } + this.selectionDirection = newSelection.selectionDirection; this.updateTextareaPosition(); }, diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 351b2416c7e..c68a1f4dccc 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 @@ -204,6 +213,32 @@ 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) { + 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; + // mimic HTMLTextareaElement behavior + this.selectionDirection = selectionDirection === 'backward' ? 'backward' : 'forward'; + // needed for future calcualtions of `selectionDirection` + this.__selectionStartOnMouseDown = this.selectionDirection === 'forward' ? + this.selectionStart : + this.selectionEnd; + changed && this._fireSelectionChanged(); + this._updateTextarea(); + }, + /** * @private * @param {String} property 'selectionStart' or 'selectionEnd' From 36bbcb1118a5ed1239c9180714fe7cdaa3d3762a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 09:20:32 +0200 Subject: [PATCH 32/61] Update itext.class.js --- src/shapes/itext.class.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index c68a1f4dccc..36d1bdd87ea 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -220,6 +220,17 @@ * @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) { @@ -229,12 +240,14 @@ var changed = selectionStart !== this.selectionStart || selectionEnd !== this.selectionEnd; this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; - // mimic HTMLTextareaElement behavior - this.selectionDirection = selectionDirection === 'backward' ? 'backward' : 'forward'; - // needed for future calcualtions of `selectionDirection` - this.__selectionStartOnMouseDown = this.selectionDirection === 'forward' ? - this.selectionStart : - this.selectionEnd; + if (selectionDirection !== false) { + // mimic HTMLTextareaElement behavior + this.selectionDirection = selectionDirection === 'backward' ? 'backward' : 'forward'; + // needed for future calcualtions of `selectionDirection` + this.__selectionStartOnMouseDown = this.selectionDirection === 'forward' ? + this.selectionStart : + this.selectionEnd; + } changed && this._fireSelectionChanged(); this._updateTextarea(); }, From befdcd859452b322bf73b9a1de8371c9774c3512 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 10:00:48 +0200 Subject: [PATCH 33/61] rtl support prep --- src/mixins/itext_behavior.mixin.js | 40 +++++------------------ src/mixins/itext_key_behavior.mixin.js | 45 ++++++++++++-------------- 2 files changed, 29 insertions(+), 56 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index ea15ad69ec9..cd676f00189 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -188,24 +188,12 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findWordBoundary: function (from, startFrom) { - return from === 'right' ? - this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current word according to current selection index * @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 @@ -220,7 +208,7 @@ index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -228,7 +216,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,24 +234,12 @@ return startFrom + offset; }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findLineBoundary: function (from, startFrom) { - return from === 'right' ? - this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current line according to current selection index * @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) { @@ -271,7 +247,7 @@ index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -279,7 +255,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) { @@ -337,8 +313,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; diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 68d9371794b..9bec904dda5 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -504,11 +504,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { - changed = this._moveLeft(e, 'selectionEnd'); + changed = this._move(e, 'selectionEnd', -1); } else if (this.selectionStart !== 0) { this._selectionDirection = 'left'; - changed = this._moveLeft(e, 'selectionStart'); + changed = this._move(e, 'selectionStart', -1); } } else { @@ -517,7 +517,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // 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) { - changed = this._moveLeft(e, 'selectionStart'); + changed = this._move(e, 'selectionStart', -1); } this.selectionEnd = this.selectionStart; } @@ -535,18 +535,18 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var changed = false; if (e.shiftKey) { if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); + changed = this._move(e, 'selectionStart', 1); } else if (this.selectionEnd !== this._text.length) { this._selectionDirection = 'right'; - changed = this._moveRight(e, 'selectionEnd'); + changed = this._move(e, 'selectionEnd', 1); } } else { changed = true; this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); + changed = this._move(e, 'selectionStart', 1); this.selectionEnd = this.selectionStart; } else { @@ -572,18 +572,29 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private - * @return {Boolean} true if a change happened + * @param {Event} e + * @param {'selectionStart'|'selectionEnd'} prop + * @param {number} direction + * @returns {boolean} true if a change happened */ _move: function(e, prop, direction) { var newValue; + direction = Math.sign(direction); + if (direction === 0) { + return false; + } if (e.altKey) { - newValue = this.findWordBoundary(direction, this[prop]); + newValue = direction > 0 ? + this.findWordBoundaryEnd(this[prop]) : + this.findWordBoundaryStart(this[prop]); } else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { - newValue = this.findLineBoundary(direction, this[prop]); + newValue = direction > 0 ? + this.findLineBoundaryEnd(this[prop]) : + this.findLineBoundaryStart(this[prop]); } else { - this[prop] += direction === 'left' ? -1 : 1; + this[prop] = Math.min(Math.max(this[prop] + direction, 0), this.text.length); return true; } if (typeof newValue !== undefined && this[prop] !== newValue) { @@ -592,20 +603,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } }, - /** - * @private - */ - _moveLeft: function(e, prop) { - return this._move(e, prop, 'left'); - }, - - /** - * @private - */ - _moveRight: function(e, prop) { - return this._move(e, prop, 'right'); - }, - /** * Removes characters from start/end * start/end ar per grapheme position in _text array. From 0551fe07b45e2eb8c0298c8a5cd7d2ebe2640549 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 10:28:49 +0200 Subject: [PATCH 34/61] forward/backward --- src/mixins/itext_behavior.mixin.js | 6 ++-- src/mixins/itext_key_behavior.mixin.js | 38 +++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index cd676f00189..d6ddd14e76a 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -900,14 +900,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 +919,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_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 9bec904dda5..316f051da49 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -57,11 +57,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorEndDir', - 36: 'moveCursorStartDir', - 37: 'moveCursorStartDir', + 35: 'moveCursorForward', + 36: 'moveCursorBackward', + 37: 'moveCursorBackward', 38: 'moveCursorUp', - 39: 'moveCursorEndDir', + 39: 'moveCursorForward', 40: 'moveCursorDown', }, @@ -70,11 +70,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorStartDir', - 36: 'moveCursorEndDir', - 37: 'moveCursorEndDir', + 35: 'moveCursorBackward', + 36: 'moveCursorForward', + 37: 'moveCursorForward', 38: 'moveCursorUp', - 39: 'moveCursorStartDir', + 39: 'moveCursorBackward', 40: 'moveCursorDown', }, @@ -470,7 +470,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); @@ -494,26 +494,26 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor left + * Moves cursor back * @param {Event} e Event object */ - moveCursorStartDir: function (e) { + moveCursorBackward: function (e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } var changed = false; if (e.shiftKey) { - if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + if (this.selectionDirection === 'forward' && this.selectionStart !== this.selectionEnd) { changed = this._move(e, 'selectionEnd', -1); } else if (this.selectionStart !== 0) { - this._selectionDirection = 'left'; + //this._selectionDirection = 'left'; changed = this._move(e, 'selectionStart', -1); } } else { changed = true; - this._selectionDirection = 'left'; + //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) { @@ -525,26 +525,26 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor right + * Moves cursor forward * @param {Event} e Event object */ - moveCursorEndDir: function (e) { + moveCursorForward: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } var changed = false; if (e.shiftKey) { - if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + 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._selectionDirection = 'right'; changed = this._move(e, 'selectionEnd', 1); } } else { changed = true; - this._selectionDirection = 'right'; + //this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { changed = this._move(e, 'selectionStart', 1); this.selectionEnd = this.selectionStart; From fdba87ffccdb74975d0df71cd618701b49f396e2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 10:54:14 +0200 Subject: [PATCH 35/61] rename --- src/mixins/itext_behavior.mixin.js | 10 +++++----- src/mixins/itext_click_behavior.mixin.js | 2 +- src/shapes/itext.class.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index d6ddd14e76a..1cdf0b09958 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -390,19 +390,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,7 +455,7 @@ return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd, - selectionDirection: graphemeStart < this.__selectionStartOnMouseDown ? 'backward' : 'forward' + selectionDirection: graphemeStart < this.__selectionStartOrigin ? 'backward' : 'forward' }; }, diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index 69b14ed9a13..f43b4069275 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/shapes/itext.class.js b/src/shapes/itext.class.js index 36d1bdd87ea..677c6460f2d 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -244,7 +244,7 @@ // mimic HTMLTextareaElement behavior this.selectionDirection = selectionDirection === 'backward' ? 'backward' : 'forward'; // needed for future calcualtions of `selectionDirection` - this.__selectionStartOnMouseDown = this.selectionDirection === 'forward' ? + this.__selectionStartOrigin = this.selectionDirection === 'forward' ? this.selectionStart : this.selectionEnd; } From ddda27385d8745aed19fce1379b2544564e7fc96 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 10:54:28 +0200 Subject: [PATCH 36/61] fix selection cases --- src/mixins/itext_key_behavior.mixin.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 316f051da49..1c426eb852f 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -508,6 +508,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } else if (this.selectionStart !== 0) { //this._selectionDirection = 'left'; + this.__selectionStartOrigin = this.selectionEnd; changed = this._move(e, 'selectionStart', -1); } } @@ -516,10 +517,16 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot //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) { - changed = this._move(e, 'selectionStart', -1); + 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.selectionEnd = this.selectionStart; } this._invalidateCursor(changed); }, @@ -539,6 +546,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } else if (this.selectionEnd !== this._text.length) { //this._selectionDirection = 'right'; + this.__selectionStartOrigin = this.selectionStart; changed = this._move(e, 'selectionEnd', 1); } } @@ -549,9 +557,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot changed = this._move(e, 'selectionStart', 1); this.selectionEnd = this.selectionStart; } - else { + else if (this.selectionDirection === 'forward') { this.selectionStart = this.selectionEnd; } + else { + this.selectionEnd = this.selectionStart; + } } this._invalidateCursor(changed); }, From dfffc0f1b80de99cf95a9d4a6ad94757abd26a5a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 11:01:35 +0200 Subject: [PATCH 37/61] Squashed commit of the following: commit ddda27385d8745aed19fce1379b2544564e7fc96 Author: ShaMan123 Date: Fri Mar 11 10:54:28 2022 +0200 fix selection cases commit fdba87ffccdb74975d0df71cd618701b49f396e2 Author: ShaMan123 Date: Fri Mar 11 10:54:14 2022 +0200 rename commit 0551fe07b45e2eb8c0298c8a5cd7d2ebe2640549 Author: ShaMan123 Date: Fri Mar 11 10:28:49 2022 +0200 forward/backward commit befdcd859452b322bf73b9a1de8371c9774c3512 Author: ShaMan123 Date: Fri Mar 11 10:00:48 2022 +0200 rtl support prep commit 36bbcb1118a5ed1239c9180714fe7cdaa3d3762a Author: ShaMan123 Date: Fri Mar 11 09:20:32 2022 +0200 Update itext.class.js commit c021097db4d9a5ba64b716d6f3854fdc0a7af3b7 Author: ShaMan123 Date: Fri Mar 11 09:03:07 2022 +0200 feat(IText): selectionDirection, selectionDirection commit 4f9529e2e73a3544cc42523c1daa18f87696c2a4 Author: amir hossein Date: Wed Feb 16 09:46:27 2022 +0330 fix(fabric.Text): consider justify text alignment in RTL text commit fb3054b5475c23bc4ca8f52de3140b378838647b Author: amir hossein Date: Sat Feb 12 13:23:07 2022 +0330 fix(fabric.Text): support RTL different text alignments commit 07f597d49db11bb0b8df70e883cd3d3eca8652ed Author: ShaMan123 Date: Thu Mar 10 20:28:48 2022 +0200 Update text.js commit 28596f5b08c3620deadc5fd3d56d66d1ce68ca68 Author: ShaMan123 Date: Thu Mar 10 20:17:45 2022 +0200 fix(tests) commit 681c0198fa4861c24b44cf21768f7ecf3b328ade Author: ShaMan123 Date: Thu Mar 10 20:05:57 2022 +0200 pathSide commit 1a3f138f6bb11e7d9bd335763b708490d77ec183 Author: ShaMan123 Date: Thu Mar 10 19:40:40 2022 +0200 lint + rename commit 3b80c7bdfd78ae58ca387e370fc675441c8af9e4 Author: ShaMan123 Date: Thu Mar 10 19:26:21 2022 +0200 cleanup commit 76f94b4ec678105609a8fd53c6b85ad7e6d8de23 Author: ShaMan123 Date: Thu Mar 10 18:56:00 2022 +0200 feat(Text): textAlign `start`, `end` and a bit of tidying up commit 2e608dcdde612cbc63f50354a7ea9df7a9dcfd6c Author: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Wed Mar 9 09:37:43 2022 +0200 feat(): dataURL export - filter objects (#7788) * feat(): filter options * visual test commit 1ea246502f12775a0de0db13115c35d93baa71e7 Author: Amirhossein Mehrabi Date: Sat Feb 26 22:36:46 2022 +0330 fix(fabric.Text): add the previous code as comment --- src/mixins/itext_behavior.mixin.js | 47 ++++++----- src/mixins/itext_click_behavior.mixin.js | 2 +- src/mixins/itext_key_behavior.mixin.js | 100 ++++++++++++----------- src/shapes/itext.class.js | 48 +++++++++++ 4 files changed, 131 insertions(+), 66 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index f50d1f67246..f04bd218b1e 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -205,7 +205,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 @@ -220,7 +220,7 @@ index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -228,7 +228,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 @@ -263,7 +263,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) { @@ -271,7 +271,7 @@ index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -279,7 +279,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) { @@ -337,8 +337,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; @@ -414,19 +414,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(); @@ -472,11 +472,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' + }; }, /** @@ -489,8 +493,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(); }, @@ -514,6 +522,7 @@ if (!this.inCompositionMode) { this.selectionStart = newSelection.selectionStart; } + this.selectionDirection = newSelection.selectionDirection; this.updateTextareaPosition(); }, @@ -915,14 +924,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 { @@ -934,7 +943,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 69b14ed9a13..f43b4069275 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 68d9371794b..1c426eb852f 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -57,11 +57,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorEndDir', - 36: 'moveCursorStartDir', - 37: 'moveCursorStartDir', + 35: 'moveCursorForward', + 36: 'moveCursorBackward', + 37: 'moveCursorBackward', 38: 'moveCursorUp', - 39: 'moveCursorEndDir', + 39: 'moveCursorForward', 40: 'moveCursorDown', }, @@ -70,11 +70,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorStartDir', - 36: 'moveCursorEndDir', - 37: 'moveCursorEndDir', + 35: 'moveCursorBackward', + 36: 'moveCursorForward', + 37: 'moveCursorForward', 38: 'moveCursorUp', - 39: 'moveCursorStartDir', + 39: 'moveCursorBackward', 40: 'moveCursorDown', }, @@ -470,7 +470,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); @@ -494,64 +494,75 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor left + * Moves cursor back * @param {Event} e Event object */ - moveCursorStartDir: function (e) { + moveCursorBackward: function (e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } var changed = false; if (e.shiftKey) { - if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { - changed = this._moveLeft(e, 'selectionEnd'); + if (this.selectionDirection === 'forward' && this.selectionStart !== this.selectionEnd) { + changed = this._move(e, 'selectionEnd', -1); } else if (this.selectionStart !== 0) { - this._selectionDirection = 'left'; - changed = this._moveLeft(e, 'selectionStart'); + //this._selectionDirection = 'left'; + this.__selectionStartOrigin = this.selectionEnd; + changed = this._move(e, 'selectionStart', -1); } } else { changed = true; - this._selectionDirection = 'left'; + //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) { - changed = this._moveLeft(e, 'selectionStart'); + 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.selectionEnd = this.selectionStart; } this._invalidateCursor(changed); }, /** - * Moves cursor right + * Moves cursor forward * @param {Event} e Event object */ - moveCursorEndDir: function (e) { + moveCursorForward: function (e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } var changed = false; if (e.shiftKey) { - if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); + if (this.selectionDirection === 'backward' && this.selectionStart !== this.selectionEnd) { + changed = this._move(e, 'selectionStart', 1); } else if (this.selectionEnd !== this._text.length) { - this._selectionDirection = 'right'; - changed = this._moveRight(e, 'selectionEnd'); + //this._selectionDirection = 'right'; + this.__selectionStartOrigin = this.selectionStart; + changed = this._move(e, 'selectionEnd', 1); } } else { changed = true; - this._selectionDirection = 'right'; + //this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); + changed = this._move(e, 'selectionStart', 1); this.selectionEnd = this.selectionStart; } - else { + else if (this.selectionDirection === 'forward') { this.selectionStart = this.selectionEnd; } + else { + this.selectionEnd = this.selectionStart; + } } this._invalidateCursor(changed); }, @@ -572,18 +583,29 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private - * @return {Boolean} true if a change happened + * @param {Event} e + * @param {'selectionStart'|'selectionEnd'} prop + * @param {number} direction + * @returns {boolean} true if a change happened */ _move: function(e, prop, direction) { var newValue; + direction = Math.sign(direction); + if (direction === 0) { + return false; + } if (e.altKey) { - newValue = this.findWordBoundary(direction, this[prop]); + newValue = direction > 0 ? + this.findWordBoundaryEnd(this[prop]) : + this.findWordBoundaryStart(this[prop]); } else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { - newValue = this.findLineBoundary(direction, this[prop]); + newValue = direction > 0 ? + this.findLineBoundaryEnd(this[prop]) : + this.findLineBoundaryStart(this[prop]); } else { - this[prop] += direction === 'left' ? -1 : 1; + this[prop] = Math.min(Math.max(this[prop] + direction, 0), this.text.length); return true; } if (typeof newValue !== undefined && this[prop] !== newValue) { @@ -592,20 +614,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } }, - /** - * @private - */ - _moveLeft: function(e, prop) { - return this._move(e, prop, 'left'); - }, - - /** - * @private - */ - _moveRight: function(e, prop) { - return this._move(e, prop, 'right'); - }, - /** * Removes characters from start/end * start/end ar per grapheme position in _text array. diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 351b2416c7e..677c6460f2d 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 @@ -204,6 +213,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' From d6b65f37138fd590783c7b6426a481a21c58cb77 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 11:25:30 +0200 Subject: [PATCH 38/61] fix(tests) --- test/unit/itext.js | 40 ++++++++++++++++---------------- test/unit/itext_key_behaviour.js | 26 ++++++++++----------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/test/unit/itext.js b/test/unit/itext.js index 6a42fb54415..00ef6f0b43f 100644 --- a/test/unit/itext.js +++ b/test/unit/itext.js @@ -444,44 +444,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 785696abd2b..5c9bfbabb67 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.moveCursorStartDir({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); + 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.moveCursorEndDir({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); + 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.moveCursorStartDir({ 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.moveCursorEndDir({ 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'); @@ -120,7 +120,7 @@ iText.selectionStart = 28; iText.selectionEnd = 31; - iText.moveCursorStartDir({ shiftKey: false }); + iText.moveCursorBackward({ shiftKey: false }); assert.equal(selection, 1, 'should fire'); assert.equal(iText.selectionStart, 28, 'should move to selection Start'); assert.equal(iText.selectionEnd, 28, 'should move to selection Start'); @@ -174,16 +174,16 @@ iText.selectionStart = 2; iText.selectionEnd = 2; - iText.moveCursorStartDir({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorStartDir'); + 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.moveCursorEndDir({ shiftKey: false}); - assert.equal(selection, 1, 'should fire once on moveCursorEndDir'); + 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; @@ -215,7 +215,7 @@ iText.selectionStart = 0; iText.selectionEnd = 1; iText._selectionDirection = 'left'; - iText.moveCursorStartDir({ shiftKey: true}); + iText.moveCursorBackward({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 0, 'should not move'); assert.equal(iText.selectionEnd, 1, 'should not move'); @@ -229,7 +229,7 @@ iText.selectionStart = 30; iText.selectionEnd = 31; iText._selectionDirection = 'right'; - iText.moveCursorEndDir({ shiftKey: true}); + iText.moveCursorForward({ shiftKey: true}); assert.equal(selection, 0, 'should not fire with no change'); assert.equal(iText.selectionStart, 30, 'should not move'); assert.equal(iText.selectionEnd, 31, 'should not move'); From d05cc0b0a4e98fd650629c1bab4dcfddf80e8f1b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 11 Mar 2022 10:00:48 +0200 Subject: [PATCH 39/61] rtl support prep --- dist/fabric.js | 2452 ++++++++++++++++++++++-------------------------- 1 file changed, 1122 insertions(+), 1330 deletions(-) diff --git a/dist/fabric.js b/dist/fabric.js index 9b7bf655d2a..f19897aee8c 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -540,32 +540,6 @@ fabric.CommonMethods = { } }, - /** - * @private - * @param {Object} [filler] Options object - * @param {String} [property] property to set the Gradient to - */ - _initGradient: function(filler, property) { - if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { - this.set(property, new fabric.Gradient(filler)); - } - }, - - /** - * @private - * @param {Object} [filler] Options object - * @param {String} [property] property to set the Pattern to - * @param {Function} [callback] callback to invoke after pattern load - */ - _initPattern: function(filler, property, callback) { - if (filler && filler.source && !(filler instanceof fabric.Pattern)) { - this.set(property, new fabric.Pattern(filler, callback)); - } - else { - callback && callback(); - } - }, - /** * @private */ @@ -629,6 +603,10 @@ fabric.CommonMethods = { PiBy180 = Math.PI / 180, PiBy2 = Math.PI / 2; + /** + * @typedef {[number,number,number,number,number,number]} Matrix + */ + /** * @namespace fabric.util */ @@ -740,7 +718,7 @@ fabric.CommonMethods = { rotatePoint: function(point, origin, radians) { var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), v = fabric.util.rotateVector(newPoint, radians); - return new fabric.Point(v.x, v.y).addEquals(origin); + return v.addEquals(origin); }, /** @@ -749,17 +727,14 @@ fabric.CommonMethods = { * @memberOf fabric.util * @param {Object} vector The vector to rotate (x and y) * @param {Number} radians The radians of the angle for the rotation - * @return {Object} The new rotated point + * @return {fabric.Point} The new rotated point */ rotateVector: function(vector, radians) { var sin = fabric.util.sin(radians), cos = fabric.util.cos(radians), rx = vector.x * cos - vector.y * sin, ry = vector.x * sin + vector.y * cos; - return { - x: rx, - y: ry - }; + return new fabric.Point(rx, ry); }, /** @@ -798,7 +773,7 @@ fabric.CommonMethods = { * @returns {Point} vector representing the unit vector of pointing to the direction of `v` */ getHatVector: function (v) { - return new fabric.Point(v.x, v.y).multiply(1 / Math.hypot(v.x, v.y)); + return new fabric.Point(v.x, v.y).scalarMultiply(1 / Math.hypot(v.x, v.y)); }, /** @@ -913,8 +888,70 @@ fabric.CommonMethods = { ); }, + /** + * Sends a point from the source coordinate plane to the destination coordinate plane.\ + * From the canvas/viewer's perspective the point remains unchanged. + * + * @example Send point from canvas plane to group plane + * var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 }); + * var group = new fabric.Group([obj], { strokeWidth: 0 }); + * var sentPoint1 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), null, group.calcTransformMatrix()); + * var sentPoint2 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), fabric.iMatrix, group.calcTransformMatrix()); + * console.log(sentPoint1, sentPoint2) // both points print (0,0) which is the center of group + * + * @static + * @memberOf fabric.util + * @see {fabric.util.transformPointRelativeToCanvas} for transforming relative to canvas + * @param {fabric.Point} point + * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `point` exists in the canvas coordinate plane. + * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `point` should be sent to the canvas coordinate plane. + * @returns {fabric.Point} transformed point + */ + sendPointToPlane: function (point, from, to) { + // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) + // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) + var inv = fabric.util.invertTransform(to || fabric.iMatrix); + var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); + return fabric.util.transformPoint(point, t); + }, + + /** + * Transform point relative to canvas. + * From the viewport/viewer's perspective the point remains unchanged. + * + * `child` relation means `point` exists in the coordinate plane created by `canvas`. + * In other words point is measured acoording to canvas' top left corner + * meaning that if `point` is equal to (0,0) it is positioned at canvas' top left corner. + * + * `sibling` relation means `point` exists in the same coordinate plane as canvas. + * In other words they both relate to the same (0,0) and agree on every point, which is how an event relates to canvas. + * + * @static + * @memberOf fabric.util + * @param {fabric.Point} point + * @param {fabric.StaticCanvas} canvas + * @param {'sibling'|'child'} relationBefore current relation of point to canvas + * @param {'sibling'|'child'} relationAfter desired relation of point to canvas + * @returns {fabric.Point} transformed point + */ + transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) { + if (relationBefore !== 'child' && relationBefore !== 'sibling') { + throw new Error('fabric.js: recieved bad argument ' + relationBefore); + } + if (relationAfter !== 'child' && relationAfter !== 'sibling') { + throw new Error('fabric.js: recieved bad argument ' + relationAfter); + } + if (relationBefore === relationAfter) { + return point; + } + var t = canvas.viewportTransform; + return fabric.util.transformPoint(point, relationAfter === 'child' ? fabric.util.invertTransform(t) : t); + }, + /** * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @static + * @memberOf fabric.util * @param {Array} points 4 points array * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix * @return {Object} Object with left, top, width, height properties @@ -1080,185 +1117,84 @@ fabric.CommonMethods = { }, /** - * Loads image element from given url and passes it to a callback + * Loads image element from given url and resolve it, or catch. * @memberOf fabric.util * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {*} [context] Context to invoke callback in - * @param {Object} [crossOrigin] crossOrigin value to set image element to - */ - loadImage: function(url, callback, context, crossOrigin) { - if (!url) { - callback && callback.call(context, url); - return; - } - - var img = fabric.util.createImage(); - - /** @ignore */ - var onLoadCallback = function () { - callback && callback.call(context, img, false); - img = img.onload = img.onerror = null; - }; - - img.onload = onLoadCallback; - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback.call(context, null, true); - img = img.onload = img.onerror = null; - }; - - // data-urls appear to be buggy with crossOrigin - // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 - // see https://code.google.com/p/chromium/issues/detail?id=315152 - // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 - // crossOrigin null is the same as not set. - if (url.indexOf('data') !== 0 && - crossOrigin !== undefined && - crossOrigin !== null) { - img.crossOrigin = crossOrigin; - } - - // IE10 / IE11-Fix: SVG contents from data: URI - // will only be available if the IMG is present - // in the DOM (and visible) - if (url.substring(0,14) === 'data:image/svg') { - img.onload = null; - fabric.util.loadImageInDom(img, onLoadCallback); - } - - img.src = url; - }, - - /** - * Attaches SVG image with data: URL to the dom - * @memberOf fabric.util - * @param {Object} img Image object with data:image/svg src - * @param {Function} callback Callback; invoked with loaded image - * @return {Object} DOM element (div containing the SVG image) - */ - loadImageInDom: function(img, onLoadCallback) { - var div = fabric.document.createElement('div'); - div.style.width = div.style.height = '1px'; - div.style.left = div.style.top = '-100%'; - div.style.position = 'absolute'; - div.appendChild(img); - fabric.document.querySelector('body').appendChild(div); - /** - * Wrap in function to: - * 1. Call existing callback - * 2. Cleanup DOM - */ - img.onload = function () { - onLoadCallback(); - div.parentNode.removeChild(div); - div = null; - }; + * @param {Object} [options] image loading options + * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous + * @param {Promise} img the loaded image. + */ + loadImage: function(url, options) { + return new Promise(function(resolve, reject) { + var img = fabric.util.createImage(); + var done = function() { + img.onload = img.onerror = null; + resolve(img); + }; + if (!url) { + done(); + } + else { + img.onload = done; + img.onerror = function () { + reject(new Error('Error loading ' + img.src)); + }; + options && options.crossOrigin && (img.crossOrigin = options.crossOrigin); + img.src = url; + } + }); }, /** * Creates corresponding fabric instances from their object representations * @static * @memberOf fabric.util - * @param {Array} objects Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created + * @param {Object[]} objects Objects to enliven * @param {String} namespace Namespace to get klass "Class" object from * @param {Function} reviver Method for further parsing of object elements, * called after each fabric object created. */ - enlivenObjects: function(objects, callback, namespace, reviver) { - objects = objects || []; - - var enlivenedObjects = [], - numLoadedObjects = 0, - numTotalObjects = objects.length; - - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - callback && callback(enlivenedObjects.filter(function(obj) { - // filter out undefined objects (objects that gave error) - return obj; - })); - } - } - - if (!numTotalObjects) { - callback && callback(enlivenedObjects); - return; - } - - objects.forEach(function (o, index) { - // if sparse array - if (!o || !o.type) { - onLoaded(); - return; - } - var klass = fabric.util.getKlass(o.type, namespace); - klass.fromObject(o, function (obj, error) { - error || (enlivenedObjects[index] = obj); - reviver && reviver(o, obj, error); - onLoaded(); + enlivenObjects: function(objects, namespace, reviver) { + return Promise.all(objects.map(function(obj) { + var klass = fabric.util.getKlass(obj.type, namespace); + return klass.fromObject(obj).then(function(fabricInstance) { + reviver && reviver(obj, fabricInstance); + return fabricInstance; }); - }); + })); }, /** * Creates corresponding fabric instances residing in an object, e.g. `clipPath` - * @see {@link fabric.Object.ENLIVEN_PROPS} - * @param {Object} object - * @param {Object} [context] assign enlived props to this object (pass null to skip this) - * @param {(objects:fabric.Object[]) => void} callback - */ - enlivenObjectEnlivables: function (object, context, callback) { - var enlivenProps = fabric.Object.ENLIVEN_PROPS.filter(function (key) { return !!object[key]; }); - fabric.util.enlivenObjects(enlivenProps.map(function (key) { return object[key]; }), function (enlivedProps) { - var objects = {}; - enlivenProps.forEach(function (key, index) { - objects[key] = enlivedProps[index]; - context && (context[key] = enlivedProps[index]); - }); - callback && callback(objects); - }); - }, - - /** - * Create and wait for loading of patterns - * @static - * @memberOf fabric.util - * @param {Array} patterns Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * called after each fabric object created. + * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path ) + * @returns {Promise} the input object with enlived values */ - enlivenPatterns: function(patterns, callback) { - patterns = patterns || []; - function onLoaded() { - if (++numLoadedPatterns === numPatterns) { - callback && callback(enlivenedPatterns); + enlivenObjectEnlivables: function (serializedObject) { + // enlive every possible property + var promises = Object.values(serializedObject).map(function(value) { + if (!value) { + return value; } - } - - var enlivenedPatterns = [], - numLoadedPatterns = 0, - numPatterns = patterns.length; - - if (!numPatterns) { - callback && callback(enlivenedPatterns); - return; - } - - patterns.forEach(function (p, index) { - if (p && p.source) { - new fabric.Pattern(p, function(pattern) { - enlivenedPatterns[index] = pattern; - onLoaded(); + if (value.colorStops) { + return new fabric.Gradient(value); + } + if (value.type) { + return fabric.util.enlivenObjects([value]).then(function (enlived) { + return enlived[0]; }); } - else { - enlivenedPatterns[index] = p; - onLoaded(); + if (value.source) { + return fabric.Pattern.fromObject(value); } + return value; + }); + var keys = Object.keys(serializedObject); + return Promise.all(promises).then(function(enlived) { + return enlived.reduce(function(acc, instance, index) { + acc[keys[index]] = instance; + return acc; + }, {}); }); }, @@ -1304,7 +1240,7 @@ fabric.CommonMethods = { * @return {Array} properties Properties names to include */ populateWithProperties: function(source, destination, properties) { - if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + if (properties && Array.isArray(properties)) { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; @@ -1753,6 +1689,50 @@ fabric.CommonMethods = { object.setPositionByOrigin(center, 'center', 'center'); }, + /** + * + * A util that abstracts applying transform to objects.\ + * Sends `object` to the destination coordinate plane by applying the relevant transformations.\ + * Changes the space/plane where `object` is drawn.\ + * From the canvas/viewer's perspective `object` remains unchanged. + * + * @example Move clip path from one object to another while preserving it's appearance as viewed by canvas/viewer + * let obj, obj2; + * let clipPath = new fabric.Circle({ radius: 50 }); + * obj.clipPath = clipPath; + * // render + * fabric.util.sendObjectToPlane(clipPath, obj.calcTransformMatrix(), obj2.calcTransformMatrix()); + * obj.clipPath = undefined; + * obj2.clipPath = clipPath; + * // render, clipPath now clips obj2 but seems unchanged from the eyes of the viewer + * + * @example Clip an object's clip path with an existing object + * let obj, existingObj; + * let clipPath = new fabric.Circle({ radius: 50 }); + * obj.clipPath = clipPath; + * let transformTo = fabric.util.multiplyTransformMatrices(obj.calcTransformMatrix(), clipPath.calcTransformMatrix()); + * fabric.util.sendObjectToPlane(existingObj, existingObj.group?.calcTransformMatrix(), transformTo); + * clipPath.clipPath = existingObj; + * + * @static + * @memberof fabric.util + * @param {fabric.Object} object + * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `object` is a direct child of canvas. + * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `object` should be sent to the canvas coordinate plane. + * @returns {Matrix} the transform matrix that was applied to `object` + */ + sendObjectToPlane: function (object, from, to) { + // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) + // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) + var inv = fabric.util.invertTransform(to || fabric.iMatrix); + var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); + fabric.util.applyTransformToObject( + object, + fabric.util.multiplyTransformMatrices(t, object.calcOwnMatrix()) + ); + return t; + }, + /** * given a width and height, return the size of the bounding box * that can contains the box with width/height with applied transform @@ -2825,7 +2805,7 @@ fabric.CommonMethods = { /** * Creates an empty object and copies all enumerable properties of another object to it - * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. * @memberOf fabric.util.object * @param {Object} object Object to clone * @param {Boolean} [deep] Whether to clone nested objects @@ -2834,7 +2814,7 @@ fabric.CommonMethods = { //TODO: this function return an empty object if you try to clone null function clone(object, deep) { - return extend({ }, object, deep); + return deep ? extend({ }, object, deep) : Object.assign({}, object); } /** @namespace fabric.util.object */ @@ -3512,6 +3492,7 @@ fabric.CommonMethods = { /** * Cross-browser abstraction for sending XMLHttpRequest * @memberOf fabric.util + * @deprecated this has to go away, we can use a modern browser method to do the same. * @param {String} url URL to send XMLHttpRequest to * @param {Object} [options] Options object * @param {String} [options.method="GET"] @@ -4382,8 +4363,7 @@ fabric.warn = console.warn; } function normalizeValue(attr, value, parentAttributes, fontSize) { - var isArray = Object.prototype.toString.call(value) === '[object Array]', - parsed; + var isArray = Array.isArray(value), parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; @@ -4781,7 +4761,7 @@ fabric.warn = console.warn; return; } - var xlink = xlinkAttribute.substr(1), + var xlink = xlinkAttribute.slice(1), x = el.getAttribute('x') || 0, y = el.getAttribute('y') || 0, el2 = elementById(doc, xlink).cloneNode(true), @@ -5053,7 +5033,7 @@ fabric.warn = console.warn; function recursivelyParseGradientsXlink(doc, gradient) { var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], xlinkAttr = 'xlink:href', - xLink = gradient.getAttribute(xlinkAttr).substr(1), + xLink = gradient.getAttribute(xlinkAttr).slice(1), referencedGradient = elementById(doc, xLink); if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { recursivelyParseGradientsXlink(doc, referencedGradient); @@ -5668,47 +5648,61 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, + /** + * Multiplies this point by another value and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + multiply: function (that) { + return new Point(this.x * that.x, this.y * that.y); + }, + /** * Multiplies this point by a value and returns a new one - * TODO: rename in scalarMultiply in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - multiply: function (scalar) { + scalarMultiply: function (scalar) { return new Point(this.x * scalar, this.y * scalar); }, /** * Multiplies this point by a value - * TODO: rename in scalarMultiplyEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - multiplyEquals: function (scalar) { + scalarMultiplyEquals: function (scalar) { this.x *= scalar; this.y *= scalar; return this; }, + /** + * Divides this point by another and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + divide: function (that) { + return new Point(this.x / that.x, this.y / that.y); + }, + /** * Divides this point by a value and returns a new one - * TODO: rename in scalarDivide in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - divide: function (scalar) { + scalarDivide: function (scalar) { return new Point(this.x / scalar, this.y / scalar); }, /** * Divides this point by a value - * TODO: rename in scalarDivideEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - divideEquals: function (scalar) { + scalarDivideEquals: function (scalar) { this.x /= scalar; this.y /= scalar; return this; @@ -6726,7 +6720,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @return {Number} 0 - 7 a quadrant number */ function findCornerQuadrant(fabricObject, control) { - var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + // angle is relative to canvas plane + var angle = fabricObject.getTotalAngle(); + var cornerAngle = angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; return Math.round((cornerAngle % 360) / 45); } @@ -6895,7 +6891,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ function wrapWithFixedAnchor(actionHandler) { return function(eventData, transform, x, y) { - var target = transform.target, centerPoint = target.getCenterPoint(), + var target = transform.target, centerPoint = target.getRelativeCenterPoint(), constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), actionPerformed = actionHandler(eventData, transform, x, y); target.setPositionByOrigin(constraint, transform.originX, transform.originY); @@ -6933,7 +6929,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp control = target.controls[transform.corner], zoom = target.canvas.getZoom(), padding = target.padding / zoom, - localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); + localPoint = target.normalizePoint(new fabric.Point(x, y), originX, originY); if (localPoint.x >= padding) { localPoint.x -= padding; } @@ -6979,7 +6975,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectX(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions(0, target.skewY), + dimNoSkew = target._getTransformedDimensions({ skewX: 0, skewY: target.skewY }), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7022,7 +7018,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectY(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions(target.skewX, 0), + dimNoSkew = target._getTransformedDimensions({ skewX: target.skewX, skewY: 0 }), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7171,7 +7167,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function rotationWithSnapping(eventData, transform, x, y) { var t = transform, target = t.target, - pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + pivotPoint = target.translateToOriginPoint(target.getRelativeCenterPoint(), t.originX, t.originY); if (target.lockRotation) { return false; @@ -7526,7 +7522,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp // this is still wrong ctx.lineWidth = 1; ctx.translate(left, top); - ctx.rotate(degreesToRadians(fabricObject.angle)); + // angle is relative to canvas plane + var angle = fabricObject.getTotalAngle(); + ctx.rotate(degreesToRadians(angle)); // this does not work, and fixed with ( && ) does not make sense. // to have real transparent corners we need the controls on upperCanvas // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); @@ -8429,30 +8427,18 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ patternTransform: null, + type: 'pattern', + /** * Constructor * @param {Object} [options] Options object - * @param {Function} [callback] function to invoke after callback init. + * @param {option.source} [source] the pattern source, eventually empty or a drawable * @return {fabric.Pattern} thisArg */ - initialize: function(options, callback) { + initialize: function(options) { options || (options = { }); - this.id = fabric.Object.__uid++; this.setOptions(options); - if (!options.source || (options.source && typeof options.source !== 'string')) { - callback && callback(this); - return; - } - else { - // img src string - var _this = this; - this.source = fabric.util.createImage(); - fabric.util.loadImage(options.source, function(img, isError) { - _this.source = img; - callback && callback(_this, isError); - }, null, this.crossOrigin); - } }, /** @@ -8564,6 +8550,15 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return ctx.createPattern(source, this.repeat); } }); + + fabric.Pattern.fromObject = function(object) { + var patternOptions = Object.assign({}, object); + return fabric.util.loadImage(object.source, { crossOrigin: object.crossOrigin }) + .then(function(img) { + patternOptions.source = img; + return new fabric.Pattern(patternOptions); + }); + }; })(); @@ -8815,7 +8810,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Background color of canvas instance. - * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. * @type {(String|fabric.Pattern)} * @default */ @@ -8833,7 +8827,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Overlay color of canvas instance. - * Should be set via {@link fabric.StaticCanvas#setOverlayColor} * @since 1.3.9 * @type {(String|fabric.Pattern)} * @default @@ -8970,7 +8963,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @param {Object} [options] Options object */ _initStatic: function(el, options) { - var cb = this.requestRenderAllBound; this._objects = []; this._createLowerCanvas(el); this._initOptions(options); @@ -8978,19 +8970,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp if (!this.interactive) { this._initRetinaScaling(); } - - if (options.overlayImage) { - this.setOverlayImage(options.overlayImage, cb); - } - if (options.backgroundImage) { - this.setBackgroundImage(options.backgroundImage, cb); - } - if (options.backgroundColor) { - this.setBackgroundColor(options.backgroundColor, cb); - } - if (options.overlayColor) { - this.setOverlayColor(options.overlayColor, cb); - } this.calcOffset(); }, @@ -9041,202 +9020,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, - /** - * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to - * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage with left/top = 0 - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * // Needed to position overlayImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example overlayImage with different properties - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top' - * }); - * @example Stretched overlayImage #1 - width/height correspond to canvas width/height - * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { - * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); - * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); - * }); - * @example Stretched overlayImage #2 - width/height correspond to canvas width/height - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * width: canvas.width, - * height: canvas.height, - * // Needed to position overlayImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example overlayImage loaded from cross-origin - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top', - * crossOrigin: 'anonymous' - * }); - */ - setOverlayImage: function (image, callback, options) { - return this.__setBgOverlayImage('overlayImage', image, callback, options); - }, - - /** - * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to - * @param {Function} callback Callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} - * @example Normal backgroundImage with left/top = 0 - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * // Needed to position backgroundImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example backgroundImage with different properties - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top' - * }); - * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height - * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { - * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); - * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); - * }); - * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * width: canvas.width, - * height: canvas.height, - * // Needed to position backgroundImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example backgroundImage loaded from cross-origin - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top', - * crossOrigin: 'anonymous' - * }); - */ - // TODO: fix stretched examples - setBackgroundImage: function (image, callback, options) { - return this.__setBgOverlayImage('backgroundImage', image, callback, options); - }, - - /** - * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas - * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to - * @param {Function} callback Callback to invoke when foreground color is set - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} - * @example Normal overlayColor - color value - * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as overlayColor - * canvas.setOverlayColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png' - * }, canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as overlayColor with repeat and offset - * canvas.setOverlayColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png', - * repeat: 'repeat', - * offsetX: 200, - * offsetY: 100 - * }, canvas.renderAll.bind(canvas)); - */ - setOverlayColor: function(overlayColor, callback) { - return this.__setBgOverlayColor('overlayColor', overlayColor, callback); - }, - - /** - * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to - * @param {Function} callback Callback to invoke when background color is set - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} - * @example Normal backgroundColor - color value - * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as backgroundColor - * canvas.setBackgroundColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png' - * }, canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as backgroundColor with repeat and offset - * canvas.setBackgroundColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png', - * repeat: 'repeat', - * offsetX: 200, - * offsetY: 100 - * }, canvas.renderAll.bind(canvas)); - */ - setBackgroundColor: function(backgroundColor, callback) { - return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); - }, - - /** - * @private - * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} - * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) - * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to - * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. - * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. - */ - __setBgOverlayImage: function(property, image, callback, options) { - if (typeof image === 'string') { - fabric.util.loadImage(image, function(img, isError) { - if (img) { - var instance = new fabric.Image(img, options); - this[property] = instance; - instance.canvas = this; - } - callback && callback(img, isError); - }, this, options && options.crossOrigin); - } - else { - options && image.setOptions(options); - this[property] = image; - image && (image.canvas = this); - callback && callback(image, false); - } - - return this; - }, - - /** - * @private - * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} - * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) - * @param {(Object|String|null)} color Object with pattern information, color value or null - * @param {Function} [callback] Callback is invoked when color is set - */ - __setBgOverlayColor: function(property, color, callback) { - this[property] = color; - this._initGradient(color, property); - this._initPattern(color, property, callback); - return this; - }, - /** * @private */ @@ -9793,6 +9576,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * Returns coordinates of a center of canvas. * Returned value is an object with top and left properties * @return {Object} object with "top" and "left" number values + * @deprecated migrate to `getCenterPoint` */ getCenter: function () { return { @@ -9801,13 +9585,21 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }; }, + /** + * Returns coordinates of a center of canvas. + * @return {fabric.Point} + */ + getCenterPoint: function () { + return new fabric.Point(this.width / 2, this.height / 2); + }, + /** * Centers object horizontally in the canvas * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + return this._centerObject(object, new fabric.Point(this.getCenterPoint().x, object.getCenterPoint().y)); }, /** @@ -9817,7 +9609,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObjectV: function (object) { - return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenterPoint().y)); }, /** @@ -9827,9 +9619,8 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObject: function(object) { - var center = this.getCenter(); - - return this._centerObject(object, new fabric.Point(center.left, center.top)); + var center = this.getCenterPoint(); + return this._centerObject(object, center); }, /** @@ -9840,7 +9631,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ viewportCenterObject: function(object) { var vpCenter = this.getVpCenter(); - return this._centerObject(object, vpCenter); }, @@ -9874,9 +9664,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ getVpCenter: function() { - var center = this.getCenter(), + var center = this.getCenterPoint(), iVpt = invertTransform(this.viewportTransform); - return transformPoint({ x: center.left, y: center.top }, iVpt); + return transformPoint(center, iVpt); }, /** @@ -11878,6 +11668,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _hoveredTargets: [], + /** + * hold the list of objects to render + * @type fabric.Object[] + * @private + */ + _objectsToRender: undefined, + /** * @private */ @@ -11895,6 +11692,23 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); }, + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function (obj) { + this._objectsToRender = undefined; + this.callSuper('_onObjectAdded', obj); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function (obj) { + this._objectsToRender = undefined; + this.callSuper('_onObjectRemoved', obj); + }, /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. @@ -11904,7 +11718,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var activeObjects = this.getActiveObjects(), object, objsToRender, activeGroupObjects; - if (activeObjects.length > 0 && !this.preserveObjectStacking) { + if (!this.preserveObjectStacking && activeObjects.length > 1) { objsToRender = []; activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { @@ -11921,6 +11735,15 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } objsToRender.push.apply(objsToRender, activeGroupObjects); } + // in case a single object is selected render it's entire above the other objects + else if (!this.preserveObjectStacking && activeObjects.length === 1) { + var target = activeObjects[0], ancestors = target.getAncestors(true); + var topAncestor = ancestors.length === 0 ? target : ancestors.pop(); + objsToRender = this._objects.slice(); + var index = objsToRender.indexOf(topAncestor); + index > -1 && objsToRender.splice(objsToRender.indexOf(topAncestor), 1); + objsToRender.push(topAncestor); + } else { objsToRender = this._objects; } @@ -11942,7 +11765,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.hasLostContext = false; } var canvasToDrawOn = this.contextContainer; - this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + !this._objectsToRender && (this._objectsToRender = this._chooseObjectsToRender()); + this.renderCanvas(canvasToDrawOn, this._objectsToRender); return this; }, @@ -12033,7 +11857,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _isSelectionKeyPressed: function(e) { var selectionKeyPressed = false; - if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { + if (Array.isArray(this.selectionKey)) { selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); } else { @@ -12148,14 +11972,22 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (!target) { return; } - - var pointer = this.getPointer(e), corner = target.__corner, + var pointer = this.getPointer(e); + if (target.group) { + // transform pointer to target's containing coordinate plane + pointer = fabric.util.transformPoint(pointer, fabric.util.invertTransform(target.group.calcTransformMatrix())); + } + var corner = target.__corner, control = target.controls[corner], actionHandler = (alreadySelected && corner) ? control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, action = this._getActionFromCorner(alreadySelected, corner, e, target), origin = this._getOriginFromCorner(target, corner), altKey = e[this.centeredKey], + /** + * relative to target's containing coordinate plane + * both agree on every point + **/ transform = { target: target, action: action, @@ -12165,7 +11997,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, - // used by transation offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, @@ -12174,11 +12005,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ey: pointer.y, lastX: pointer.x, lastY: pointer.y, - // unsure they are useful anymore. - // left: target.left, - // top: target.top, theta: degreesToRadians(target.angle), - // end of unsure width: target.width * target.scaleX, shiftKey: e.shiftKey, altKey: altKey, @@ -12271,11 +12098,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { return activeObject; } - if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (aObjects.length > 1 && activeObject.type === 'activeSelection' + && !skipGroup && this.searchPossibleTargets([activeObject], pointer)) { return activeObject; } if (aObjects.length === 1 && - activeObject === this._searchPossibleTargets([activeObject], pointer)) { + activeObject === this.searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { return activeObject; } @@ -12285,7 +12113,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.targets = []; } } - var target = this._searchPossibleTargets(this._objects, pointer); + var target = this.searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; this.targets = activeTargetSubs; @@ -12322,10 +12150,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * Internal Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @param {Array} [objects] objects array to look into * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} object that contains pointer + * @return {fabric.Object} **top most object from given `objects`** that contains pointer * @private */ _searchPossibleTargets: function(objects, pointer) { @@ -12349,6 +12177,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return target; }, + /** + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * @see {@link fabric.Canvas#_searchPossibleTargets} + * @param {Array} [objects] objects array to look into + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @return {fabric.Object} **top most object on screen** that contains pointer + */ + searchPossibleTargets: function (objects, pointer) { + var target = this._searchPossibleTargets(objects, pointer); + return target; + }, + /** * Returns pointer coordinates without the effect of the viewport * @param {Object} pointer with "x" and "y" number values @@ -12364,27 +12204,27 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * Returns pointer coordinates relative to canvas. * Can return coordinates with or without viewportTransform. - * ignoreZoom false gives back coordinates that represent + * ignoreVpt false gives back coordinates that represent * the point clicked on canvas element. - * ignoreZoom true gives back coordinates after being processed + * ignoreVpt true gives back coordinates after being processed * by the viewportTransform ( sort of coordinates of what is displayed * on the canvas where you are clicking. - * ignoreZoom true = HTMLElement coordinates relative to top,left - * ignoreZoom false, default = fabric space coordinates, the same used for shape position - * To interact with your shapes top and left you want to use ignoreZoom true - * most of the time, while ignoreZoom false will give you coordinates + * ignoreVpt true = HTMLElement coordinates relative to top,left + * ignoreVpt false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreVpt true + * most of the time, while ignoreVpt false will give you coordinates * compatible with the object.oCoords system. * of the time. * @param {Event} e - * @param {Boolean} ignoreZoom + * @param {Boolean} ignoreVpt * @return {Object} object with "x" and "y" number values */ - getPointer: function (e, ignoreZoom) { + getPointer: function (e, ignoreVpt) { // return cached values if we are in the event processing chain - if (this._absolutePointer && !ignoreZoom) { + if (this._absolutePointer && !ignoreVpt) { return this._absolutePointer; } - if (this._pointer && ignoreZoom) { + if (this._pointer && ignoreVpt) { return this._pointer; } @@ -12407,7 +12247,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; - if (!ignoreZoom) { + if (!ignoreVpt) { pointer = this.restorePointerVpt(pointer); } @@ -12459,6 +12299,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.contextTop = upperCanvasEl.getContext('2d'); }, + /** + * Returns context of top canvas where interactions are drawn + * @returns {CanvasRenderingContext2D} + */ + getTopContext: function () { + return this.contextTop; + }, + /** * @private */ @@ -12583,7 +12431,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _fireSelectionEvents: function(oldObjects, e) { var somethingChanged = false, objects = this.getActiveObjects(), - added = [], removed = []; + added = [], removed = [], invalidate = false; oldObjects.forEach(function(oldObject) { if (objects.indexOf(oldObject) === -1) { somethingChanged = true; @@ -12605,6 +12453,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } }); if (oldObjects.length > 0 && objects.length > 0) { + invalidate = true; somethingChanged && this.fire('selection:updated', { e: e, selected: added, @@ -12612,17 +12461,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }); } else if (objects.length > 0) { + invalidate = true; this.fire('selection:created', { e: e, selected: added, }); } else if (oldObjects.length > 0) { + invalidate = true; this.fire('selection:cleared', { e: e, deselected: removed, }); } + invalidate && (this._objectsToRender = undefined); }, /** @@ -14007,8 +13859,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @see {@link https://jsfiddle.net/xsjua1rd/ demo} * @example Generate jpeg dataURL with lower quality * var dataURL = canvas.toDataURL({ * format: 'jpeg', @@ -14027,6 +13880,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * format: 'png', * multiplier: 2 * }); + * @example Generate dataURL with objects that overlap a specified object + * var myObject; + * var dataURL = canvas.toDataURL({ + * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject) + * }); */ toDataURL: function (options) { options || (options = { }); @@ -14045,29 +13903,31 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * This is an intermediary step used to get to a dataUrl but also it is useful to * create quick image copies of a canvas without passing for the dataUrl string * @param {Number} [multiplier] a zoom factor. - * @param {Object} [cropping] Cropping informations - * @param {Number} [cropping.left] Cropping left offset. - * @param {Number} [cropping.top] Cropping top offset. - * @param {Number} [cropping.width] Cropping width. - * @param {Number} [cropping.height] Cropping height. - */ - toCanvasElement: function(multiplier, cropping) { + * @param {Object} [options] Cropping informations + * @param {Number} [options.left] Cropping left offset. + * @param {Number} [options.top] Cropping top offset. + * @param {Number} [options.width] Cropping width. + * @param {Number} [options.height] Cropping height. + * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. + */ + toCanvasElement: function (multiplier, options) { multiplier = multiplier || 1; - cropping = cropping || { }; - var scaledWidth = (cropping.width || this.width) * multiplier, - scaledHeight = (cropping.height || this.height) * multiplier, + options = options || { }; + var scaledWidth = (options.width || this.width) * multiplier, + scaledHeight = (options.height || this.height) * multiplier, zoom = this.getZoom(), originalWidth = this.width, originalHeight = this.height, newZoom = zoom * multiplier, vp = this.viewportTransform, - translateX = (vp[4] - (cropping.left || 0)) * multiplier, - translateY = (vp[5] - (cropping.top || 0)) * multiplier, + translateX = (vp[4] - (options.left || 0)) * multiplier, + translateY = (vp[5] - (options.top || 0)) * multiplier, originalInteractive = this.interactive, newVp = [newZoom, 0, 0, newZoom, translateX, translateY], originalRetina = this.enableRetinaScaling, canvasEl = fabric.util.createCanvasElement(), - originalContextTop = this.contextTop; + originalContextTop = this.contextTop, + objectsToRender = options.filter ? this._objects.filter(options.filter) : this._objects; canvasEl.width = scaledWidth; canvasEl.height = scaledHeight; this.contextTop = null; @@ -14077,7 +13937,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.width = scaledWidth; this.height = scaledHeight; this.calcViewportBoundaries(); - this.renderCanvas(canvasEl.getContext('2d'), this._objects); + this.renderCanvas(canvasEl.getContext('2d'), objectsToRender); this.viewportTransform = vp; this.width = originalWidth; this.height = originalHeight; @@ -14097,24 +13957,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} * @param {String|Object} json JSON string or object - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: {@link fabric.Image}) - * are initialized * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. - * @return {fabric.Canvas} instance + * @return {Promise} instance * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} * @example loadFromJSON - * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll()); * @example loadFromJSON with reviver - * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * canvas.loadFromJSON(json, function(o, object) { * // `o` = json object * // `object` = fabric.Object instance * // ... do some stuff ... + * }).then((canvas) => { + * ... canvas is restored, add your code. * }); */ - loadFromJSON: function (json, callback, reviver) { + loadFromJSON: function (json, reviver) { if (!json) { return; } @@ -14125,38 +13984,35 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati : fabric.util.object.clone(json); var _this = this, - clipPath = serialized.clipPath, renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; - delete serialized.clipPath; - - this._enlivenObjects(serialized.objects, function (enlivenedObjects) { - _this.clear(); - _this._setBgOverlay(serialized, function () { - if (clipPath) { - _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { - _this.clipPath = enlivenedCanvasClip[0]; - _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) + .then(function(enlived) { + _this.clear(); + return fabric.util.enlivenObjectEnlivables({ + backgroundImage: serialized.backgroundImage, + backgroundColor: serialized.background, + overlayImage: serialized.overlayImage, + overlayColor: serialized.overlay, + clipPath: serialized.clipPath, + }) + .then(function(enlivedMap) { + _this.__setupCanvas(serialized, enlived, renderOnAddRemove); + _this.set(enlivedMap); + return _this; }); - } - else { - _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); - } }); - }, reviver); - return this; }, /** * @private * @param {Object} serialized Object with background and overlay information - * @param {Array} restored canvas objects - * @param {Function} cached renderOnAddRemove callback - * @param {Function} callback Invoked after all background and overlay images/patterns loaded + * @param {Array} enlivenedObjects canvas objects + * @param {boolean} renderOnAddRemove renderOnAddRemove setting for the canvas */ - __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove) { var _this = this; enlivenedObjects.forEach(function(obj, index) { // we splice the array just in case some custom classes restored from JSON @@ -14175,149 +14031,41 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // create the Object instance. Here the Canvas is // already an instance and we are just loading things over it this._setOptions(serialized); - this.renderAll(); - callback && callback(); }, /** - * @private - * @param {Object} serialized Object with background and overlay information - * @param {Function} callback Invoked after all background and overlay images/patterns loaded - */ - _setBgOverlay: function(serialized, callback) { - var loaded = { - backgroundColor: false, - overlayColor: false, - backgroundImage: false, - overlayImage: false - }; - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { - callback && callback(); - return; - } - - var cbIfLoaded = function () { - if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { - callback && callback(); - } - }; - - this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); - this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); - this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); - this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + * Clones canvas instance + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + * @returns {Promise} + */ + clone: function (properties) { + var data = JSON.stringify(this.toJSON(properties)); + return this.cloneWithoutData().then(function(clone) { + return clone.loadFromJSON(data); + }); }, /** - * @private - * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) - * @param {(Object|String)} value Value to set - * @param {Object} loaded Set loaded property to true if property is set - * @param {Object} callback Callback function to invoke after property is set + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @returns {Promise} */ - __setBgOverlay: function(property, value, loaded, callback) { - var _this = this; - - if (!value) { - loaded[property] = true; - callback && callback(); - return; - } - - if (property === 'backgroundImage' || property === 'overlayImage') { - fabric.util.enlivenObjects([value], function(enlivedObject){ - _this[property] = enlivedObject[0]; - loaded[property] = true; - callback && callback(); - }); - } - else { - this['set' + fabric.util.string.capitalize(property, true)](value, function() { - loaded[property] = true; - callback && callback(); - }); - } - }, - - /** - * @private - * @param {Array} objects - * @param {Function} callback - * @param {Function} [reviver] - */ - _enlivenObjects: function (objects, callback, reviver) { - if (!objects || objects.length === 0) { - callback && callback([]); - return; - } - - fabric.util.enlivenObjects(objects, function(enlivenedObjects) { - callback && callback(enlivenedObjects); - }, null, reviver); - }, - - /** - * @private - * @param {String} format - * @param {Function} callback - */ - _toDataURL: function (format, callback) { - this.clone(function (clone) { - callback(clone.toDataURL(format)); - }); - }, - - /** - * @private - * @param {String} format - * @param {Number} multiplier - * @param {Function} callback - */ - _toDataURLWithMultiplier: function (format, multiplier, callback) { - this.clone(function (clone) { - callback(clone.toDataURLWithMultiplier(format, multiplier)); - }); - }, - - /** - * Clones canvas instance - * @param {Object} [callback] Receives cloned instance as a first argument - * @param {Array} [properties] Array of properties to include in the cloned canvas and children - */ - clone: function (callback, properties) { - var data = JSON.stringify(this.toJSON(properties)); - this.cloneWithoutData(function(clone) { - clone.loadFromJSON(data, function() { - callback && callback(clone); - }); - }); - }, - - /** - * Clones canvas instance without cloning existing data. - * This essentially copies canvas dimensions, clipping properties, etc. - * but leaves data empty (so that you can populate it with your own) - * @param {Object} [callback] Receives cloned instance as a first argument - */ - cloneWithoutData: function(callback) { + cloneWithoutData: function() { var el = fabric.util.createCanvasElement(); el.width = this.width; el.height = this.height; - + // this seems wrong. either Canvas or StaticCanvas var clone = new fabric.Canvas(el); + var data = {}; if (this.backgroundImage) { - clone.setBackgroundImage(this.backgroundImage.src, function() { - clone.renderAll(); - callback && callback(clone); - }); - clone.backgroundImageOpacity = this.backgroundImageOpacity; - clone.backgroundImageStretch = this.backgroundImageStretch; + data.backgroundImage = this.backgroundImage.toObject(); } - else { - callback && callback(clone); + if (this.backgroundColor) { + data.background = this.backgroundColor.toObject ? this.backgroundColor.toObject() : this.backgroundColor; } + return clone.loadFromJSON(data); } }); @@ -15051,17 +14799,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _getCacheCanvasDimensions: function() { var objectScale = this.getTotalObjectScaling(), // caculate dimensions without skewing - dim = this._getTransformedDimensions(0, 0), - neededX = dim.x * objectScale.scaleX / this.scaleX, - neededY = dim.y * objectScale.scaleY / this.scaleY; + dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), + neededX = dim.x * objectScale.x / this.scaleX, + neededY = dim.y * objectScale.y / this.scaleY; return { // for sure this ALIASING_LIMIT is slightly creating problem // in situation in which the cache canvas gets an upper limit // also objectScale contains already scaleX and scaleY width: neededX + ALIASING_LIMIT, height: neededY + ALIASING_LIMIT, - zoomX: objectScale.scaleX, - zoomY: objectScale.scaleY, + zoomX: objectScale.x, + zoomY: objectScale.y, x: neededX, y: neededY }; @@ -15139,10 +14887,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ setOptions: function(options) { this._setOptions(options); - this._initGradient(options.fill, 'fill'); - this._initGradient(options.stroke, 'stroke'); - this._initPattern(options.fill, 'fill'); - this._initPattern(options.stroke, 'stroke'); }, /** @@ -15236,11 +14980,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (object[prop] === prototype[prop]) { delete object[prop]; } - var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && - Object.prototype.toString.call(prototype[prop]) === '[object Array]'; - // basically a check for [] === [] - if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + if (Array.isArray(object[prop]) && Array.isArray(prototype[prop]) + && object[prop].length === 0 && prototype[prop].length === 0) { delete object[prop]; } }); @@ -15258,7 +15000,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Return the object scale factor counting also the group scaling - * @return {Object} object with scaleX and scaleY properties + * @return {fabric.Point} */ getObjectScaling: function() { // if the object is a top level one, on the canvas, we go for simple aritmetic @@ -15266,14 +15008,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // and will likely kill the cache when not needed // https://github.com/fabricjs/fabric.js/issues/7157 if (!this.group) { - return { - scaleX: this.scaleX, - scaleY: this.scaleY, - }; + return new fabric.Point(Math.abs(this.scaleX), Math.abs(this.scaleY)); } // if we are inside a group total zoom calculation is complex, we defer to generic matrices var options = fabric.util.qrDecompose(this.calcTransformMatrix()); - return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; + return new fabric.Point(Math.abs(options.scaleX), Math.abs(options.scaleY)); }, /** @@ -15281,14 +15020,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Object} object with scaleX and scaleY properties */ getTotalObjectScaling: function() { - var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + var scale = this.getObjectScaling(); if (this.canvas) { var zoom = this.canvas.getZoom(); var retina = this.canvas.getRetinaScaling(); - scaleX *= zoom * retina; - scaleY *= zoom * retina; + scale.scalarMultiplyEquals(zoom * retina); } - return { scaleX: scaleX, scaleY: scaleY }; + return scale; }, /** @@ -15303,6 +15041,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return opacity; }, + /** + * Returns the object angle relative to canvas counting also the group property + * @returns {number} + */ + getTotalAngle: function () { + return this.group ? + fabric.util.qrDecompose(this.calcTransformMatrix()).angle : + this.angle; + }, + /** * @private * @param {String} key @@ -15346,16 +15094,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this; }, - /** - * This callback function is called by the parent group of an object every - * time a non-delegated property changes on the group. It is passed the key - * and value as parameters. Not adding in this function's signature to avoid - * Travis build error about unused variables. - */ - setOnGroup: function() { - // implemented by sub-classes, as needed. - }, - /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform @@ -15416,7 +15154,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati renderCache: function(options) { options = options || {}; - if (!this._cacheCanvas) { + if (!this._cacheCanvas || !this._cacheContext) { this._createCacheCanvas(); } if (this.isCacheDirty()) { @@ -15431,6 +15169,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _removeCacheCanvas: function() { this._cacheCanvas = null; + this._cacheContext = null; this.cacheWidth = 0; this.cacheHeight = 0; }, @@ -15503,6 +15242,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Check if this object or a child object will cast a shadow * used by Group.shouldCache to know if child has a shadow recursively * @return {Boolean} + * @deprecated */ willDrawShadow: function() { return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); @@ -15589,7 +15329,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (this.isNotVisible()) { return false; } - if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { + if (this._cacheCanvas && this._cacheContext && !skipCanvas && this._updateCacheCanvas()) { // in this case the context is already cleared. return true; } @@ -15598,7 +15338,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati (this.clipPath && this.clipPath.absolutePositioned) || (this.statefullCache && this.hasStateChanged('cacheProperties')) ) { - if (this._cacheCanvas && !skipCanvas) { + if (this._cacheCanvas && this._cacheContext && !skipCanvas) { var width = this.cacheWidth / this.zoomX; var height = this.cacheHeight / this.zoomY; this._cacheContext.clearRect(-width / 2, -height / 2, width, height); @@ -15754,24 +15494,19 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return; } - var shadow = this.shadow, canvas = this.canvas, scaling, + var shadow = this.shadow, canvas = this.canvas, multX = (canvas && canvas.viewportTransform[0]) || 1, - multY = (canvas && canvas.viewportTransform[3]) || 1; - if (shadow.nonScaling) { - scaling = { scaleX: 1, scaleY: 1 }; - } - else { - scaling = this.getObjectScaling(); - } + multY = (canvas && canvas.viewportTransform[3]) || 1, + scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); if (canvas && canvas._isRetinaScaling()) { multX *= fabric.devicePixelRatio; multY *= fabric.devicePixelRatio; } ctx.shadowColor = shadow.color; ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * - (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; - ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; - ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; + (multX + multY) * (scaling.x + scaling.y) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.x; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.y; }, /** @@ -15874,12 +15609,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati } ctx.save(); - if (this.strokeUniform && this.group) { + if (this.strokeUniform) { var scaling = this.getObjectScaling(); - ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); - } - else if (this.strokeUniform) { - ctx.scale(1 / this.scaleX, 1 / this.scaleY); + ctx.scale(1 / scaling.x, 1 / scaling.y); } this._setLineDash(ctx, this.strokeDashArray); this._setStrokeStyles(ctx, this); @@ -15981,18 +15713,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Clones an instance, using a callback method will work for every object. - * @param {Function} callback Callback is invoked with a clone as a first argument + * Clones an instance. * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @returns {Promise} */ - clone: function(callback, propertiesToInclude) { + clone: function(propertiesToInclude) { var objectForm = this.toObject(propertiesToInclude); - if (this.constructor.fromObject) { - this.constructor.fromObject(objectForm, callback); - } - else { - fabric.Object._fromObject('Object', objectForm, callback); - } + return this.constructor.fromObject(objectForm); }, /** @@ -16002,9 +15729,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * and format option. toCanvasElement is faster and produce no loss of quality. * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. * toCanvasElement and then toBlob from the obtained canvas is also a good option. - * This method is sync now, but still support the callback because we did not want to break. - * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. - * @param {Function} callback callback, invoked with an instance as a first argument * @param {Object} [options] for clone as image, passed to toDataURL * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 @@ -16014,14 +15738,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {fabric.Object} thisArg + * @return {fabric.Image} Object cloned as image. */ - cloneAsImage: function(callback, options) { + cloneAsImage: function(options) { var canvasEl = this.toCanvasElement(options); - if (callback) { - callback(new fabric.Image(canvasEl)); - } - return this; + return new fabric.Image(canvasEl); }, /** @@ -16055,21 +15776,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var el = fabric.util.createCanvasElement(), // skip canvas zoom and calculate with setCoords now. boundingRect = this.getBoundingRect(true, true), - shadow = this.shadow, scaling, - shadowOffset = { x: 0, y: 0 }, shadowBlur, + shadow = this.shadow, shadowOffset = { x: 0, y: 0 }, width, height; if (shadow) { - shadowBlur = shadow.blur; - if (shadow.nonScaling) { - scaling = { scaleX: 1, scaleY: 1 }; - } - else { - scaling = this.getObjectScaling(); - } + var shadowBlur = shadow.blur; + var scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); // consider non scaling shadow. - shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); - shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.x)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.y)); } width = boundingRect.width + shadowOffset.x; height = boundingRect.height + shadowOffset.y; @@ -16132,7 +15847,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} */ isType: function(type) { - return this.type === type; + return arguments.length > 1 ? Array.from(arguments).includes(this.type) : this.type === type; }, /** @@ -16242,23 +15957,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Returns coordinates of a pointer relative to an object - * @param {Event} e Event to operate upon - * @param {Object} [pointer] Pointer to operate upon (instead of event) - * @return {Object} Coordinates of a pointer (x, y) + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. */ - getLocalPointer: function(e, pointer) { - pointer = pointer || this.canvas.getPointer(e); - var pClicked = new fabric.Point(pointer.x, pointer.y), - objectLeftTop = this._getLeftTopCoords(); - if (this.angle) { - pClicked = fabric.util.rotatePoint( - pClicked, objectLeftTop, degreesToRadians(-this.angle)); - } - return { - x: pClicked.x - objectLeftTop.x, - y: pClicked.y - objectLeftTop.y - }; + setOnGroup: function() { + // implemented by sub-classes, as needed. }, /** @@ -16304,25 +16009,19 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @constant * @type string[] */ - fabric.Object.ENLIVEN_PROPS = ['clipPath']; - fabric.Object._fromObject = function(className, object, callback, extraParam) { - var klass = fabric[className]; - object = clone(object, true); - fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { - if (typeof patterns[0] !== 'undefined') { - object.fill = patterns[0]; - } - if (typeof patterns[1] !== 'undefined') { - object.stroke = patterns[1]; - } - fabric.util.enlivenObjectEnlivables(object, object, function () { - var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); - callback && callback(instance); - }); + fabric.Object._fromObject = function(klass, object, extraParam) { + var serializedObject = clone(object, true); + return fabric.util.enlivenObjectEnlivables(serializedObject).then(function(enlivedMap) { + var newObject = Object.assign(object, enlivedMap); + return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject); }); }; + fabric.Object.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Object, object); + }; + /** * Unique id used internally when creating SVG elements * @static @@ -16347,53 +16046,52 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati bottom: 0.5 }; + /** + * @typedef {number | 'left' | 'center' | 'right'} OriginX + * @typedef {number | 'top' | 'center' | 'bottom'} OriginY + */ + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Resolves origin value relative to center + * @private + * @param {OriginX} originX + * @returns number + */ + resolveOriginX: function (originX) { + return typeof originX === 'string' ? + originXOffset[originX] : + originX - 0.5; + }, + + /** + * Resolves origin value relative to center + * @private + * @param {OriginY} originY + * @returns number + */ + resolveOriginY: function (originY) { + return typeof originY === 'string' ? + originYOffset[originY] : + originY - 0.5; + }, + /** * Translates the coordinates from a set of origin to another (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { var x = point.x, y = point.y, - offsetX, offsetY, dim; - - if (typeof fromOriginX === 'string') { - fromOriginX = originXOffset[fromOriginX]; - } - else { - fromOriginX -= 0.5; - } - - if (typeof toOriginX === 'string') { - toOriginX = originXOffset[toOriginX]; - } - else { - toOriginX -= 0.5; - } - - offsetX = toOriginX - fromOriginX; - - if (typeof fromOriginY === 'string') { - fromOriginY = originYOffset[fromOriginY]; - } - else { - fromOriginY -= 0.5; - } - - if (typeof toOriginY === 'string') { - toOriginY = originYOffset[toOriginY]; - } - else { - toOriginY -= 0.5; - } - - offsetY = toOriginY - fromOriginY; + dim, + offsetX = this.resolveOriginX(toOriginX) - this.resolveOriginX(fromOriginX), + offsetY = this.resolveOriginY(toOriginY) - this.resolveOriginY(fromOriginY); if (offsetX || offsetY) { dim = this._getTransformedDimensions(); @@ -16407,8 +16105,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Translates the coordinates from origin to center coordinates (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { @@ -16422,8 +16120,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Translates the coordinates from center to origin coordinates (based on the object's dimensions) * @param {fabric.Point} center The point which corresponds to center of the object - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { @@ -16435,12 +16133,30 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Returns the real center coordinates of the object + * Returns the center coordinates of the object relative to canvas * @return {fabric.Point} */ getCenterPoint: function() { - var leftTop = new fabric.Point(this.left, this.top); - return this.translateToCenterPoint(leftTop, this.originX, this.originY); + var relCenter = this.getRelativeCenterPoint(); + return this.group ? + fabric.util.transformPoint(relCenter, this.group.calcTransformMatrix()) : + relCenter; + }, + + /** + * Returns the center coordinates of the object relative to it's containing group or null + * @return {fabric.Point|null} point or null of object has no parent group + */ + getCenterPointRelativeToParent: function () { + return this.group ? this.getRelativeCenterPoint() : null; + }, + + /** + * Returns the center coordinates of the object relative to it's parent + * @return {fabric.Point} + */ + getRelativeCenterPoint: function () { + return this.translateToCenterPoint(new fabric.Point(this.left, this.top), this.originX, this.originY); }, /** @@ -16454,26 +16170,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Returns the coordinates of the object as if it has a different origin - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ getPointByOrigin: function(originX, originY) { - var center = this.getCenterPoint(); + var center = this.getRelativeCenterPoint(); return this.translateToOriginPoint(center, originX, originY); }, /** - * Returns the point in local coordinates - * @param {fabric.Point} point The point relative to the global coordinate system - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * Returns the normalized point (rotated relative to center) in local coordinates + * @param {fabric.Point} point The point relative to instance coordinate system + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ - toLocalPoint: function(point, originX, originY) { - var center = this.getCenterPoint(), - p, p2; - + normalizePoint: function(point, originX, originY) { + var center = this.getRelativeCenterPoint(), p, p2; if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); } @@ -16488,6 +16202,20 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return p2.subtractEquals(p); }, + /** + * Returns coordinates of a pointer relative to object's top left corner in object's plane + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function (e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + return fabric.util.transformPoint( + new fabric.Point(pointer.x, pointer.y), + fabric.util.invertTransform(this.calcTransformMatrix()) + ).addEquals(new fabric.Point(this.width / 2, this.height / 2)); + }, + /** * Returns the point in global coordinates * @param {fabric.Point} The point relative to the local coordinate system @@ -16500,8 +16228,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Sets the position of the object taking into consideration the object's origin * @param {fabric.Point} pos The new position of the object - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {void} */ setPositionByOrigin: function(pos, originX, originY) { @@ -16549,7 +16277,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this._originalOriginX = this.originX; this._originalOriginY = this.originY; - var center = this.getCenterPoint(); + var center = this.getRelativeCenterPoint(); this.originX = 'center'; this.originY = 'center'; @@ -16565,7 +16293,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _resetOrigin: function() { var originPoint = this.translateToOriginPoint( - this.getCenterPoint(), + this.getRelativeCenterPoint(), this._originalOriginX, this._originalOriginY); @@ -16583,7 +16311,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @private */ _getLeftTopCoords: function() { - return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + return this.translateToOriginPoint(this.getRelativeCenterPoint(), 'left', 'top'); }, }); @@ -17023,7 +16751,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati calcLineCoords: function() { var vpt = this.getViewportTransform(), - padding = this.padding, angle = degreesToRadians(this.angle), + padding = this.padding, angle = degreesToRadians(this.getTotalAngle()), cos = util.cos(angle), sin = util.sin(angle), cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); @@ -17053,7 +16781,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var rotateMatrix = this._calcRotateMatrix(), translateMatrix = this._calcTranslateMatrix(), vpt = this.getViewportTransform(), - startMatrix = multiplyMatrices(vpt, translateMatrix), + startMatrix = this.group ? multiplyMatrices(vpt, this.group.calcTransformMatrix()) : vpt, + startMatrix = multiplyMatrices(startMatrix, translateMatrix), finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), dim = this._calculateCurrentDimensions(), @@ -17063,15 +16792,18 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }); // debug code - // var canvas = this.canvas; - // setTimeout(function() { - // canvas.contextTop.clearRect(0, 0, 700, 700); - // canvas.contextTop.fillStyle = 'green'; - // Object.keys(coords).forEach(function(key) { - // var control = coords[key]; - // canvas.contextTop.fillRect(control.x, control.y, 3, 3); - // }); - // }, 50); + /* + var canvas = this.canvas; + setTimeout(function () { + if (!canvas) return; + canvas.contextTop.clearRect(0, 0, 700, 700); + canvas.contextTop.fillStyle = 'green'; + Object.keys(coords).forEach(function(key) { + var control = coords[key]; + canvas.contextTop.fillRect(control.x, control.y, 3, 3); + }); + }, 50); + */ return coords; }, @@ -17128,7 +16860,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Array} rotation matrix for the object */ _calcTranslateMatrix: function() { - var center = this.getCenterPoint(); + var center = this.getRelativeCenterPoint(); return [1, 0, 0, 1, center.x, center.y]; }, @@ -17193,77 +16925,62 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return cache.value; }, - /* + /** * Calculate object dimensions from its properties * @private - * @return {Object} .x width dimension - * @return {Object} .y height dimension + * @returns {fabric.Point} dimensions */ _getNonTransformedDimensions: function() { - var strokeWidth = this.strokeWidth, - w = this.width + strokeWidth, - h = this.height + strokeWidth; - return { x: w, y: h }; + return new fabric.Point(this.width, this.height).scalarAddEquals(this.strokeWidth); }, - /* + /** * Calculate object bounding box dimensions from its properties scale, skew. - * @param {Number} skewX, a value to override current skewX - * @param {Number} skewY, a value to override current skewY + * @param {Object} [options] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewY] * @private - * @return {Object} .x width dimension - * @return {Object} .y height dimension + * @returns {fabric.Point} dimensions */ - _getTransformedDimensions: function(skewX, skewY) { - if (typeof skewX === 'undefined') { - skewX = this.skewX; - } - if (typeof skewY === 'undefined') { - skewY = this.skewY; - } - var dimensions, dimX, dimY, - noSkew = skewX === 0 && skewY === 0; - + _getTransformedDimensions: function (options) { + options = Object.assign({ + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + }, options || {}); + // stroke is applied before/after transformations are applied according to `strokeUniform` + var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = this.strokeWidth; if (this.strokeUniform) { - dimX = this.width; - dimY = this.height; + preScalingStrokeValue = 0; + postScalingStrokeValue = strokeWidth; } else { - dimensions = this._getNonTransformedDimensions(); - dimX = dimensions.x; - dimY = dimensions.y; + preScalingStrokeValue = strokeWidth; + postScalingStrokeValue = 0; } + var dimX = this.width + preScalingStrokeValue, + dimY = this.height + preScalingStrokeValue, + finalDimensions, + noSkew = options.skewX === 0 && options.skewY === 0; if (noSkew) { - return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + finalDimensions = new fabric.Point(dimX * options.scaleX, dimY * options.scaleY); + } + else { + var bbox = util.sizeAfterTransform(dimX, dimY, options); + finalDimensions = new fabric.Point(bbox.x, bbox.y); } - var bbox = util.sizeAfterTransform(dimX, dimY, { - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: skewX, - skewY: skewY, - }); - return this._finalizeDimensions(bbox.x, bbox.y); - }, - /* - * Calculate object bounding box dimensions from its properties scale, skew. - * @param Number width width of the bbox - * @param Number height height of the bbox - * @private - * @return {Object} .x finalized width dimension - * @return {Object} .y finalized height dimension - */ - _finalizeDimensions: function(width, height) { - return this.strokeUniform ? - { x: width + this.strokeWidth, y: height + this.strokeWidth } - : - { x: width, y: height }; + return finalDimensions.scalarAddEquals(postScalingStrokeValue); }, - /* + /** * Calculate object dimensions for controls box, including padding and canvas zoom. * and active selection - * private + * @private + * @returns {fabric.Point} dimensions */ _calculateCurrentDimensions: function() { var vpt = this.getViewportTransform(), @@ -17275,6 +16992,25 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati })(); +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * + * @param {boolean} [strict] returns only ancestors that are objects (without canvas) + * @returns {(fabric.Object | fabric.StaticCanvas)[]} ancestors from bottom to top + */ + getAncestors: function (strict) { + var ancestors = []; + var parent = this.group || (!strict ? this.canvas : undefined); + while (parent) { + ancestors.push(parent); + parent = parent.group || (!strict ? parent.canvas : undefined); + } + return ancestors; + } +}); + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** @@ -17738,15 +17474,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(pointer, forTouch) { - // objects in group, anykind, are not self modificable, - // must not return an hovered corner. - if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { + if (!this.hasControls || (!this.canvas || this.canvas._activeObject !== this)) { return false; } - - var ex = pointer.x, - ey = pointer.y, - xPoints, + var xPoints, lines, keys = Object.keys(this.oCoords), j = keys.length - 1, i; this.__corner = 0; @@ -17773,7 +17504,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); + xPoints = this._findCrossPoints(pointer, lines); if (xPoints !== 0 && xPoints % 2 === 1) { this.__corner = i; return i; @@ -17829,7 +17560,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; } ctx.save(); - var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + var center = this.getRelativeCenterPoint(), wh = this._calculateCurrentDimensions(), vpt = this.canvas.viewportTransform; ctx.translate(center.x, center.y); ctx.scale(1 / vpt[0], 1 / vpt[3]); @@ -17856,8 +17587,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = wh.x + strokeWidth, height = wh.y + strokeWidth, hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls, - shouldStroke = false; + styleOverride.hasControls : this.hasControls; ctx.save(); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17869,26 +17599,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); + hasControls && this.drawControlsConnectingLines(ctx); - if (hasControls) { - ctx.beginPath(); - this.forEachControl(function(control, key, fabricObject) { - // in this moment, the ctx is centered on the object. - // width and height of the above function are the size of the bbox. - if (control.withConnection && control.getVisibility(fabricObject, key)) { - // reset movement for each control - shouldStroke = true; - ctx.moveTo(control.x * width, control.y * height); - ctx.lineTo( - control.x * width + control.offsetX, - control.y * height + control.offsetY - ); - } - }); - if (shouldStroke) { - ctx.stroke(); - } - } ctx.restore(); return this; }, @@ -17912,7 +17624,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, height = - bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls; ctx.save(); this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17922,11 +17636,46 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); + hasControls && this.drawControlsConnectingLines(ctx); ctx.restore(); return this; }, + /** + * Draws lines from a borders of an object's bounding box to controls that have `withConnection` property set. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawControlsConnectingLines: function (ctx) { + var wh = this._calculateCurrentDimensions(), + strokeWidth = this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth, + shouldStroke = false; + + ctx.beginPath(); + this.forEachControl(function (control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + shouldStroke && ctx.stroke(); + + return this; + }, + /** * Draws corners of an object's bounding box. * Requires public properties: width, height @@ -17939,7 +17688,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot drawControls: function(ctx, styleOverride) { styleOverride = styleOverride || {}; ctx.save(); - var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; + var retinaScaling = this.canvas.getRetinaScaling(), p; ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; if (!this.transparentCorners) { @@ -17947,20 +17696,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); this.setCoords(); - if (this.group) { - // fabricJS does not really support drawing controls inside groups, - // this piece of code here helps having at least the control in places. - // If an application needs to show some objects as selected because of some UI state - // can still call Object._renderControls() on any object they desire, independently of groups. - // using no padding, circular controls and hiding the rotating cursor is higly suggested, - matrix = this.group.calcTransformMatrix(); - } this.forEachControl(function(control, key, fabricObject) { - p = fabricObject.oCoords[key]; if (control.getVisibility(fabricObject, key)) { - if (matrix) { - p = fabric.util.transformPoint(p, matrix); - } + p = fabricObject.oCoords[key]; control.render(ctx, p.x, p.y, styleOverride, fabricObject); } }); @@ -18070,7 +17808,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, startValue: object.left, - endValue: this.getCenter().left, + endValue: this.getCenterPoint().x, duration: this.FX_DURATION, onChange: function(value) { object.set('left', value); @@ -18103,7 +17841,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, startValue: object.top, - endValue: this.getCenter().top, + endValue: this.getCenterPoint().y, duration: this.FX_DURATION, onChange: function(value) { object.set('top', value); @@ -18561,16 +18299,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Line * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument + * @returns {Promise} */ - fabric.Line.fromObject = function(object, callback) { - function _callback(instance) { - delete instance.points; - callback && callback(instance); - }; + fabric.Line.fromObject = function(object) { var options = clone(object, true); options.points = [object.x1, object.y1, object.x2, object.y2]; - fabric.Object._fromObject('Line', options, _callback, 'points'); + return fabric.Object._fromObject(fabric.Line, options, 'points').then(function(fabricLine) { + delete fabricLine.points; + return fabricLine; + }); }; /** @@ -18803,11 +18540,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Circle * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - * @return {void} + * @returns {Promise} */ - fabric.Circle.fromObject = function(object, callback) { - fabric.Object._fromObject('Circle', object, callback); + fabric.Circle.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Circle, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -18899,10 +18635,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Triangle * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument + * @returns {Promise} */ - fabric.Triangle.fromObject = function(object, callback) { - return fabric.Object._fromObject('Triangle', object, callback); + fabric.Triangle.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Triangle, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19081,11 +18817,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - * @return {void} + * @returns {Promise} */ - fabric.Ellipse.fromObject = function(object, callback) { - fabric.Object._fromObject('Ellipse', object, callback); + fabric.Ellipse.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Ellipse, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19271,10 +19006,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Rect * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + * @returns {Promise} */ - fabric.Rect.fromObject = function(object, callback) { - return fabric.Object._fromObject('Rect', object, callback); + fabric.Rect.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Rect, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19541,10 +19276,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polyline * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @returns {Promise} */ - fabric.Polyline.fromObject = function(object, callback) { - return fabric.Object._fromObject('Polyline', object, callback, 'points'); + fabric.Polyline.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Polyline, object, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19623,11 +19358,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polygon * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - * @return {void} + * @returns {Promise} */ - fabric.Polygon.fromObject = function(object, callback) { - fabric.Object._fromObject('Polygon', object, callback, 'points'); + fabric.Polygon.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Polygon, object, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19642,7 +19376,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot max = fabric.util.array.max, extend = fabric.util.object.extend, clone = fabric.util.object.clone, - _toString = Object.prototype.toString, toFixed = fabric.util.toFixed; if (fabric.Path) { @@ -19696,10 +19429,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Object} [options] Options object */ _setPath: function (path, options) { - var fromArray = _toString.call(path) === '[object Array]'; - this.path = fabric.util.makePathSimpler( - fromArray ? path : fabric.util.parsePath(path) + Array.isArray(path) ? path : fabric.util.parsePath(path) ); fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); @@ -19970,20 +19701,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Path * @param {Object} object - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - */ - fabric.Path.fromObject = function(object, callback) { - if (typeof object.sourcePath === 'string') { - var pathUrl = object.sourcePath; - fabric.loadSVGFromURL(pathUrl, function (elements) { - var path = elements[0]; - path.setOptions(object); - callback && callback(path); - }); - } - else { - fabric.Object._fromObject('Path', object, callback, 'path'); - } + * @returns {Promise} + */ + fabric.Path.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Path, object, 'path'); }; /* _FROM_SVG_START_ */ @@ -20133,7 +19854,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change */ _updateObjectsCoords: function(center) { - var center = center || this.getCenterPoint(); + var center = center || this.getRelativeCenterPoint(); for (var i = this._objects.length; i--; ){ this._updateObjectCoords(this._objects[i], center); } @@ -20575,26 +20296,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Group * @param {Object} object Object to create a group from - * @param {Function} [callback] Callback to invoke when an group instance is created + * @returns {Promise} */ - fabric.Group.fromObject = function(object, callback) { - var objects = object.objects, + fabric.Group.fromObject = function(object) { + var objects = object.objects || [], options = fabric.util.object.clone(object, true); delete options.objects; - if (typeof objects === 'string') { - // it has to be an url or something went wrong. - fabric.loadSVGFromURL(objects, function (elements) { - var group = fabric.util.groupSVGElements(elements, object, objects); - group.set(options); - callback && callback(group); - }); - return; - } - fabric.util.enlivenObjects(objects, function (enlivenedObjects) { - var options = fabric.util.object.clone(object, true); - delete options.objects; - fabric.util.enlivenObjectEnlivables(object, options, function () { - callback && callback(new fabric.Group(enlivenedObjects, options, true)); + return fabric.util.enlivenObjects(objects).then(function (enlivenedObjects) { + return fabric.util.enlivenObjectEnlivables(options).then(function(enlivedProps) { + return new fabric.Group(enlivenedObjects, Object.assign(options, enlivedProps), true); }); }); }; @@ -20747,12 +20457,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.ActiveSelection * @param {Object} object Object to create a group from - * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + * @returns {Promise} */ - fabric.ActiveSelection.fromObject = function(object, callback) { - fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { - delete object.objects; - callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + fabric.ActiveSelection.fromObject = function(object) { + var objects = object.objects, + options = fabric.util.object.clone(object, true); + delete options.objects; + return fabric.util.enlivenObjects(objects).then(function(enlivenedObjects) { + return new fabric.ActiveSelection(enlivenedObjects, object, true); }); }; @@ -20903,7 +20615,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Please check video element events for seeking. * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element * @param {Object} [options] Options object - * @param {function} [callback] callback function to call after eventual filters applied. * @return {fabric.Image} thisArg */ initialize: function(element, options) { @@ -21131,20 +20842,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Sets source of an image * @param {String} src Source string (URL) - * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) * @param {Object} [options] Options object * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @return {fabric.Image} thisArg - * @chainable + * @return {Promise} thisArg */ - setSrc: function(src, callback, options) { - fabric.util.loadImage(src, function(img, isError) { - this.setElement(img, options); - this._setWidthHeight(); - callback && callback(this, isError); - }, this, options && options.crossOrigin); - return this; + setSrc: function(src, options) { + var _this = this; + return fabric.util.loadImage(src, options).then(function(img) { + _this.setElement(img, options); + _this._setWidthHeight(); + return _this; + }); }, /** @@ -21159,8 +20868,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var filter = this.resizeFilter, minimumScale = this.minimumScaleTrigger, objectScale = this.getTotalObjectScaling(), - scaleX = objectScale.scaleX, - scaleY = objectScale.scaleY, + scaleX = objectScale.x, + scaleY = objectScale.y, elementToFilter = this._filteredEl || this._originalElement; if (this.group) { this.set('dirty', true); @@ -21316,7 +21025,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _needsResize: function() { var scale = this.getTotalObjectScaling(); - return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); + return (scale.x !== this._lastScaleX || scale.y !== this._lastScaleY); }, /** @@ -21348,22 +21057,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._setWidthHeight(options); }, - /** - * @private - * @param {Array} filters to be initialized - * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created - */ - _initFilters: function(filters, callback) { - if (filters && filters.length) { - fabric.util.enlivenObjects(filters, function(enlivenedObjects) { - callback && callback(enlivenedObjects); - }, 'fabric.Image.filters'); - } - else { - callback && callback(); - } - }, - /** * @private * Set the width and the height of the image object, using the element or the @@ -21461,39 +21154,39 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} callback Callback to invoke when an image instance is created - */ - fabric.Image.fromObject = function(_object, callback) { - var object = fabric.util.object.clone(_object); - fabric.util.loadImage(object.src, function(img, isError) { - if (isError) { - callback && callback(null, true); - return; - } - fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { - object.filters = filters || []; - fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { - object.resizeFilter = resizeFilters[0]; - fabric.util.enlivenObjectEnlivables(object, object, function () { - var image = new fabric.Image(img, object); - callback(image, false); - }); - }); + * @returns {Promise} + */ + fabric.Image.fromObject = function(_object) { + var object = fabric.util.object.clone(_object), + filters = object.filters, + resizeFilter = object.resizeFilter; + // the generic enliving will fail on filters for now + delete object.resizeFilter; + delete object.filters; + return Promise.all([ + fabric.util.loadImage(object.src, { crossOrigin: _object.crossOrigin }), + filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'), + resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'), + fabric.util.enlivenObjectEnlivables(object), + ]) + .then(function(imgAndFilters) { + object.filters = imgAndFilters[1] || []; + object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; + return new fabric.Image(imgAndFilters[0], Object.assign(object, imgAndFilters[3])); }); - }, null, object.crossOrigin); }; /** * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from - * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. * @param {Object} [imgOptions] Options object + * @returns {Promise} */ - fabric.Image.fromURL = function(url, callback, imgOptions) { - fabric.util.loadImage(url, function(img, isError) { - callback && callback(new fabric.Image(img, imgOptions), isError); - }, null, imgOptions && imgOptions.crossOrigin); + fabric.Image.fromURL = function(url, imgOptions) { + return fabric.util.loadImage(url, imgOptions || {}).then(function(img) { + return new fabric.Image(img, imgOptions); + }); }; /* _FROM_SVG_START_ */ @@ -21517,8 +21210,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ fabric.Image.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, - extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + fabric.Image.fromURL(parsedAttributes['xlink:href'], Object.assign({ }, options || { }, parsedAttributes)) + .then(function(fabricImage) { + callback(fabricImage); + }); }; /* _FROM_SVG_END_ */ @@ -22428,10 +22123,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag } }); -fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { - var filter = new fabric.Image.filters[object.type](object); - callback && callback(filter); - return filter; +/** + * Create filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @returns {Promise} + */ +fabric.Image.filters.BaseFilter.fromObject = function(object) { + return Promise.resolve(new fabric.Image.filters[object.type](object)); }; @@ -22586,11 +22285,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] function to invoke after filter creation - * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + * @returns {Promise} */ fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); @@ -22700,11 +22398,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + * @returns {Promise} */ fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23054,11 +22751,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + * @returns {Promise} */ fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23210,11 +22906,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + * @returns {Promise} */ fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23322,11 +23017,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + * @returns {Promise} */ fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23459,11 +23153,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + * @returns {Promise} */ fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23598,11 +23291,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + * @returns {Promise} */ fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23773,11 +23465,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite + * @returns {Promise} */ fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24113,11 +23804,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor + * @returns {Promise} */ fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24356,17 +24046,16 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} callback to be invoked after filter creation - * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage + * @returns {Promise} */ - fabric.Image.filters.BlendImage.fromObject = function(object, callback) { - fabric.Image.fromObject(object.image, function(image) { + fabric.Image.filters.BlendImage.fromObject = function(object) { + return fabric.Image.fromObject(object.image).then(function(image) { var options = fabric.util.object.clone(object); options.image = image; - callback(new fabric.Image.filters.BlendImage(options)); + return new fabric.Image.filters.BlendImage(options); }); }; @@ -24854,11 +24543,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + * @returns {Promise} */ fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24969,11 +24657,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + * @returns {Promise} */ fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25029,7 +24716,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Saturation value, from -1 to 1. * Increases/decreases the color saturation. * A value of 0 has no effect. - * + * * @param {Number} saturation * @default */ @@ -25090,11 +24777,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate + * @returns {Promise} */ fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25151,7 +24837,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Vibrance value, from -1 to 1. * Increases/decreases the saturation of more muted colors with less effect on saturated colors. * A value of 0 has no effect. - * + * * @param {Number} vibrance * @default */ @@ -25214,11 +24900,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance + * @returns {Promise} */ fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25437,7 +25122,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Deserialize a JSON definition of a BlurFilter into a concrete instance. + * Create filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @returns {Promise} */ filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25571,11 +25259,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma + * @returns {Promise} */ fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25644,14 +25331,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Deserialize a JSON definition of a ComposedFilter into a concrete instance. */ - fabric.Image.filters.Composed.fromObject = function(object, callback) { - var filters = object.subFilters || [], - subFilters = filters.map(function(filter) { - return new fabric.Image.filters[filter.type](filter); - }), - instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); - callback && callback(instance); - return instance; + fabric.Image.filters.Composed.fromObject = function(object) { + var filters = object.subFilters || []; + return Promise.all(filters.map(function(filter) { + return fabric.Image.filters[filter.type].fromObject(filter); + })).then(function(enlivedFilters) { + return new fabric.Image.filters.Composed({ subFilters: enlivedFilters }); + }); }; })(typeof exports !== 'undefined' ? exports : this); @@ -25754,11 +25440,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation + * @returns {Promise} */ fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25889,12 +25574,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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". @@ -26002,10 +25687,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * 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 @@ -26145,6 +25830,35 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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 @@ -26572,7 +26286,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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++) { @@ -26595,17 +26310,31 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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; @@ -26643,7 +26372,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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); }, /** @@ -26811,7 +26540,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); + // this was changed in the PR #7674 + // currentDirection = ctx.canvas.getAttribute('dir'); + drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); @@ -27051,29 +26782,35 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ _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') { - leftOffset -= lineDiff; + switch (textAlign) { + case 'start': + case 'justify': + case 'justify-start': + leftOffset = 0; + 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; }, @@ -27466,22 +27203,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @memberOf fabric.Text * @param {Object} object plain js Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created + * @returns {Promise} */ - fabric.Text.fromObject = function(object, callback) { - var objectCopy = clone(object), path = object.path; - delete objectCopy.path; - return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { - if (path) { - fabric.Object._fromObject('Path', path, function(pathInstance) { - textInstance.set('path', pathInstance); - callback(textInstance); - }, 'path'); - } - else { - callback(textInstance); - } - }, 'text'); + fabric.Text.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Text, object, 'text'); }; fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; @@ -27818,16 +27543,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { (function() { - - function parseDecoration(object) { - if (object.textDecoration) { - object.textDecoration.indexOf('underline') > -1 && (object.underline = true); - object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); - object.textDecoration.indexOf('overline') > -1 && (object.overline = true); - delete object.textDecoration; - } - } - /** * IText class (introduced in v1.4) Events are also fired with "text:" * prefix when observing canvas. @@ -27897,6 +27612,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ 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 @@ -28033,6 +27757,45 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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' @@ -28185,7 +27948,31 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - boundaries.left *= -1; + 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; return this.cursorOffsetCache; @@ -28274,7 +28061,31 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - drawStart = this.width - drawStart - drawWidth; + 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( drawStart, @@ -28326,18 +28137,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @memberOf fabric.IText * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as argument + * @returns {Promise} */ - fabric.IText.fromObject = function(object, callback) { - parseDecoration(object); - if (object.styles) { - for (var i in object.styles) { - for (var j in object.styles[i]) { - parseDecoration(object.styles[i][j]); - } - } - } - fabric.Object._fromObject('IText', object, callback, 'text'); + fabric.IText.fromObject = function(object) { + return fabric.Object._fromObject(fabric.IText, object, 'text'); }; })(); @@ -28537,7 +28340,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @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 @@ -28552,7 +28355,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -28560,7 +28363,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @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 @@ -28583,7 +28386,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @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) { @@ -28591,7 +28394,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { index--; } - return startFrom - offset; + return Math.max(startFrom - offset, 0); }, /** @@ -28599,7 +28402,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @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) { @@ -28657,8 +28460,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ 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; @@ -28734,19 +28537,19 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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(); @@ -28792,11 +28595,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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' + }; }, /** @@ -28809,8 +28616,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } 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(); }, @@ -28834,6 +28645,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { if (!this.inCompositionMode) { this.selectionStart = newSelection.selectionStart; } + this.selectionDirection = newSelection.selectionDirection; this.updateTextareaPosition(); }, @@ -29235,14 +29047,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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 { @@ -29254,7 +29066,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { if (end === start) { this._selectionDirection = 'right'; } - else if (this._selectionDirection === 'left') { + else if (this.selectionDirection === 'backward') { this._selectionDirection = 'right'; this.selectionStart = end; } @@ -29392,7 +29204,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(); } @@ -29513,7 +29325,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = this._getLineLeftOffset(lineIndex); + lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -29521,7 +29333,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; @@ -29620,11 +29432,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', }, @@ -29633,11 +29445,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', }, @@ -30033,7 +29845,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); @@ -30057,111 +29869,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(); @@ -30170,35 +29957,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; }, /** @@ -30312,6 +30100,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), + (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -30334,6 +30123,9 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); + if (this.direction === 'rtl') { + lineOffset += this.width; + } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } @@ -30952,10 +30744,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @static * @memberOf fabric.Textbox * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created + * @returns {Promise} */ - fabric.Textbox.fromObject = function(object, callback) { - return fabric.Object._fromObject('Textbox', object, callback, 'text'); + fabric.Textbox.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Textbox, object, 'text'); }; })(typeof exports !== 'undefined' ? exports : this); From f09a6bec31091f14d4ce97c56c84e3db6ec08d57 Mon Sep 17 00:00:00 2001 From: amir hossein Date: Wed, 16 Feb 2022 10:31:19 +0330 Subject: [PATCH 40/61] fix(fabric.Text): fix justified RTL text issue in export to SVG --- src/mixins/itext.svg_export.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index d31404991ce..326b03c7a53 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -153,7 +153,12 @@ textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - textLeftOffset += boxWidth; + if (this.direction === 'rtl') { + textLeftOffset -= boxWidth; + } + else { + textLeftOffset += boxWidth; + } boxWidth = 0; } } From 54efaf77d8861bf0771534884deea292cff9dafe Mon Sep 17 00:00:00 2001 From: Amirhossein Mehrabi Date: Sat, 26 Feb 2022 22:36:46 +0330 Subject: [PATCH 41/61] fix(fabric.Text): add the previous code as comment From 92a0ba1fa674fd3be462a51d7ec972928c766f89 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 18:56:00 +0200 Subject: [PATCH 42/61] feat(Text): textAlign `start`, `end` and a bit of tidying up --- dist/fabric.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dist/fabric.js b/dist/fabric.js index f19897aee8c..655b5300208 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -30200,7 +30200,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - textLeftOffset += boxWidth; + if (this.direction === 'rtl') { + textLeftOffset -= boxWidth; + } + else { + textLeftOffset += boxWidth; + } boxWidth = 0; } } From a32d0c1c9c14129f8136c815708e81193f637e93 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:26:21 +0200 Subject: [PATCH 43/61] cleanup --- dist/fabric.js | 24 ++++++++++++++++++++++++ src/mixins/itext_behavior.mixin.js | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/dist/fabric.js b/dist/fabric.js index 655b5300208..4bbb8e5e740 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -28335,6 +28335,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findWordBoundary: function (from, startFrom) { + return from === 'right' ? + this.findWordBoundaryRight(startFrom) : + this.findWordBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index @@ -28381,6 +28393,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { return startFrom + offset; }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findLineBoundary: function (from, startFrom) { + return from === 'right' ? + this.findLineBoundaryRight(startFrom) : + this.findLineBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 1cdf0b09958..58dad86e3f7 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -188,6 +188,18 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findWordBoundary: function (from, startFrom) { + return from === 'right' ? + this.findWordBoundaryRight(startFrom) : + this.findWordBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index @@ -234,6 +246,18 @@ return startFrom + offset; }, + /** + * + * @param {'left'|'right'} from + * @param {number} startFrom + * @return {number} New selection index + */ + findLineBoundary: function (from, startFrom) { + return from === 'right' ? + this.findLineBoundaryRight(startFrom) : + this.findLineBoundaryLeft(startFrom) + }, + /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index From a5a723be25073178aa68b1a28f3f24ef3b8a7b0e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 19:40:40 +0200 Subject: [PATCH 44/61] lint + rename --- dist/fabric.js | 40 ++++++++++++++++++++++++------ src/mixins/itext_behavior.mixin.js | 16 ++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/dist/fabric.js b/dist/fabric.js index 4bbb8e5e740..2006bab458d 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -28336,15 +28336,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findWordBoundary: function (from, startFrom) { return from === 'right' ? this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom) + this.findWordBoundaryLeft(startFrom); }, /** @@ -28394,15 +28394,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findLineBoundary: function (from, startFrom) { return from === 'right' ? this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom) + this.findLineBoundaryLeft(startFrom); }, /** @@ -29456,11 +29456,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', +<<<<<<< HEAD 35: 'moveCursorForward', 36: 'moveCursorBackward', 37: 'moveCursorBackward', 38: 'moveCursorUp', 39: 'moveCursorForward', +======= + 35: 'moveCursorEndDir', + 36: 'moveCursorStartDir', + 37: 'moveCursorStartDir', + 38: 'moveCursorUp', + 39: 'moveCursorEndDir', +>>>>>>> ff0f8654 (lint + rename) 40: 'moveCursorDown', }, @@ -29469,11 +29477,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', +<<<<<<< HEAD 35: 'moveCursorBackward', 36: 'moveCursorForward', 37: 'moveCursorForward', 38: 'moveCursorUp', 39: 'moveCursorBackward', +======= + 35: 'moveCursorStartDir', + 36: 'moveCursorEndDir', + 37: 'moveCursorEndDir', + 38: 'moveCursorUp', + 39: 'moveCursorStartDir', +>>>>>>> ff0f8654 (lint + rename) 40: 'moveCursorDown', }, @@ -29896,7 +29912,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor back * @param {Event} e Event object */ +<<<<<<< HEAD moveCursorBackward: function (e) { +======= + moveCursorStartDir: function (e) { +>>>>>>> ff0f8654 (lint + rename) if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } @@ -29934,7 +29954,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor forward * @param {Event} e Event object */ +<<<<<<< HEAD moveCursorForward: function (e) { +======= + moveCursorEndDir: function (e) { +>>>>>>> ff0f8654 (lint + rename) if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 58dad86e3f7..f04bd218b1e 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -189,15 +189,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findWordBoundary: function (from, startFrom) { return from === 'right' ? this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom) + this.findWordBoundaryLeft(startFrom); }, /** @@ -247,15 +247,15 @@ }, /** - * - * @param {'left'|'right'} from - * @param {number} startFrom + * + * @param {'left'|'right'} from + * @param {number} startFrom * @return {number} New selection index */ findLineBoundary: function (from, startFrom) { return from === 'right' ? this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom) + this.findLineBoundaryLeft(startFrom); }, /** From 4f4c4b95f1f81acc21b88387068d116a26709c59 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:05:57 +0200 Subject: [PATCH 45/61] pathSide From 6fbbeef74b8b0cee4d81979920f621efed8b756c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:17:45 +0200 Subject: [PATCH 46/61] fix(tests) --- dist/fabric.js | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/dist/fabric.js b/dist/fabric.js index 2006bab458d..1958a3691d4 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -29456,19 +29456,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', -<<<<<<< HEAD 35: 'moveCursorForward', 36: 'moveCursorBackward', 37: 'moveCursorBackward', 38: 'moveCursorUp', 39: 'moveCursorForward', -======= - 35: 'moveCursorEndDir', - 36: 'moveCursorStartDir', - 37: 'moveCursorStartDir', - 38: 'moveCursorUp', - 39: 'moveCursorEndDir', ->>>>>>> ff0f8654 (lint + rename) 40: 'moveCursorDown', }, @@ -29477,19 +29469,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', -<<<<<<< HEAD 35: 'moveCursorBackward', 36: 'moveCursorForward', 37: 'moveCursorForward', 38: 'moveCursorUp', 39: 'moveCursorBackward', -======= - 35: 'moveCursorStartDir', - 36: 'moveCursorEndDir', - 37: 'moveCursorEndDir', - 38: 'moveCursorUp', - 39: 'moveCursorStartDir', ->>>>>>> ff0f8654 (lint + rename) 40: 'moveCursorDown', }, @@ -29912,11 +29896,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor back * @param {Event} e Event object */ -<<<<<<< HEAD moveCursorBackward: function (e) { -======= - moveCursorStartDir: function (e) { ->>>>>>> ff0f8654 (lint + rename) if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } @@ -29933,7 +29913,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } else { changed = true; +<<<<<<< HEAD //this._selectionDirection = 'left'; +======= + this._selectionDirection = 'left'; +>>>>>>> 89582310 (fix(tests)) // only move cursor when there is no selection, // otherwise we discard it, and leave cursor on same place if (this.selectionEnd === this.selectionStart) { @@ -29954,11 +29938,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Moves cursor forward * @param {Event} e Event object */ -<<<<<<< HEAD moveCursorForward: function (e) { -======= - moveCursorEndDir: function (e) { ->>>>>>> ff0f8654 (lint + rename) if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } @@ -29975,7 +29955,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } else { changed = true; +<<<<<<< HEAD //this._selectionDirection = 'right'; +======= + this._selectionDirection = 'right'; +>>>>>>> 89582310 (fix(tests)) if (this.selectionStart === this.selectionEnd) { changed = this._move(e, 'selectionStart', 1); this.selectionEnd = this.selectionStart; From 84f7ea45767b63e6c8857c46b2196ae546c34a45 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 10 Mar 2022 20:32:51 +0200 Subject: [PATCH 47/61] Update text.js From b1d124a7869b6d29034ceaeb44f746ce69b26a71 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 07:28:16 +0300 Subject: [PATCH 48/61] revert --- dist/fabric.js | 2491 ++++++++++++++++++++++++++---------------------- 1 file changed, 1331 insertions(+), 1160 deletions(-) diff --git a/dist/fabric.js b/dist/fabric.js index 1958a3691d4..9b7bf655d2a 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -540,6 +540,32 @@ fabric.CommonMethods = { } }, + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Gradient to + */ + _initGradient: function(filler, property) { + if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { + this.set(property, new fabric.Gradient(filler)); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Pattern to + * @param {Function} [callback] callback to invoke after pattern load + */ + _initPattern: function(filler, property, callback) { + if (filler && filler.source && !(filler instanceof fabric.Pattern)) { + this.set(property, new fabric.Pattern(filler, callback)); + } + else { + callback && callback(); + } + }, + /** * @private */ @@ -603,10 +629,6 @@ fabric.CommonMethods = { PiBy180 = Math.PI / 180, PiBy2 = Math.PI / 2; - /** - * @typedef {[number,number,number,number,number,number]} Matrix - */ - /** * @namespace fabric.util */ @@ -718,7 +740,7 @@ fabric.CommonMethods = { rotatePoint: function(point, origin, radians) { var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), v = fabric.util.rotateVector(newPoint, radians); - return v.addEquals(origin); + return new fabric.Point(v.x, v.y).addEquals(origin); }, /** @@ -727,14 +749,17 @@ fabric.CommonMethods = { * @memberOf fabric.util * @param {Object} vector The vector to rotate (x and y) * @param {Number} radians The radians of the angle for the rotation - * @return {fabric.Point} The new rotated point + * @return {Object} The new rotated point */ rotateVector: function(vector, radians) { var sin = fabric.util.sin(radians), cos = fabric.util.cos(radians), rx = vector.x * cos - vector.y * sin, ry = vector.x * sin + vector.y * cos; - return new fabric.Point(rx, ry); + return { + x: rx, + y: ry + }; }, /** @@ -773,7 +798,7 @@ fabric.CommonMethods = { * @returns {Point} vector representing the unit vector of pointing to the direction of `v` */ getHatVector: function (v) { - return new fabric.Point(v.x, v.y).scalarMultiply(1 / Math.hypot(v.x, v.y)); + return new fabric.Point(v.x, v.y).multiply(1 / Math.hypot(v.x, v.y)); }, /** @@ -888,70 +913,8 @@ fabric.CommonMethods = { ); }, - /** - * Sends a point from the source coordinate plane to the destination coordinate plane.\ - * From the canvas/viewer's perspective the point remains unchanged. - * - * @example Send point from canvas plane to group plane - * var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 }); - * var group = new fabric.Group([obj], { strokeWidth: 0 }); - * var sentPoint1 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), null, group.calcTransformMatrix()); - * var sentPoint2 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), fabric.iMatrix, group.calcTransformMatrix()); - * console.log(sentPoint1, sentPoint2) // both points print (0,0) which is the center of group - * - * @static - * @memberOf fabric.util - * @see {fabric.util.transformPointRelativeToCanvas} for transforming relative to canvas - * @param {fabric.Point} point - * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `point` exists in the canvas coordinate plane. - * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `point` should be sent to the canvas coordinate plane. - * @returns {fabric.Point} transformed point - */ - sendPointToPlane: function (point, from, to) { - // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) - // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) - var inv = fabric.util.invertTransform(to || fabric.iMatrix); - var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); - return fabric.util.transformPoint(point, t); - }, - - /** - * Transform point relative to canvas. - * From the viewport/viewer's perspective the point remains unchanged. - * - * `child` relation means `point` exists in the coordinate plane created by `canvas`. - * In other words point is measured acoording to canvas' top left corner - * meaning that if `point` is equal to (0,0) it is positioned at canvas' top left corner. - * - * `sibling` relation means `point` exists in the same coordinate plane as canvas. - * In other words they both relate to the same (0,0) and agree on every point, which is how an event relates to canvas. - * - * @static - * @memberOf fabric.util - * @param {fabric.Point} point - * @param {fabric.StaticCanvas} canvas - * @param {'sibling'|'child'} relationBefore current relation of point to canvas - * @param {'sibling'|'child'} relationAfter desired relation of point to canvas - * @returns {fabric.Point} transformed point - */ - transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) { - if (relationBefore !== 'child' && relationBefore !== 'sibling') { - throw new Error('fabric.js: recieved bad argument ' + relationBefore); - } - if (relationAfter !== 'child' && relationAfter !== 'sibling') { - throw new Error('fabric.js: recieved bad argument ' + relationAfter); - } - if (relationBefore === relationAfter) { - return point; - } - var t = canvas.viewportTransform; - return fabric.util.transformPoint(point, relationAfter === 'child' ? fabric.util.invertTransform(t) : t); - }, - /** * Returns coordinates of points's bounding rectangle (left, top, width, height) - * @static - * @memberOf fabric.util * @param {Array} points 4 points array * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix * @return {Object} Object with left, top, width, height properties @@ -1117,84 +1080,185 @@ fabric.CommonMethods = { }, /** - * Loads image element from given url and resolve it, or catch. + * Loads image element from given url and passes it to a callback * @memberOf fabric.util * @param {String} url URL representing an image - * @param {Object} [options] image loading options - * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous - * @param {Promise} img the loaded image. - */ - loadImage: function(url, options) { - return new Promise(function(resolve, reject) { - var img = fabric.util.createImage(); - var done = function() { - img.onload = img.onerror = null; - resolve(img); - }; - if (!url) { - done(); - } - else { - img.onload = done; - img.onerror = function () { - reject(new Error('Error loading ' + img.src)); - }; - options && options.crossOrigin && (img.crossOrigin = options.crossOrigin); - img.src = url; - } - }); + * @param {Function} callback Callback; invoked with loaded image + * @param {*} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } + + var img = fabric.util.createImage(); + + /** @ignore */ + var onLoadCallback = function () { + callback && callback.call(context, img, false); + img = img.onload = img.onerror = null; + }; + + img.onload = onLoadCallback; + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + // crossOrigin null is the same as not set. + if (url.indexOf('data') !== 0 && + crossOrigin !== undefined && + crossOrigin !== null) { + img.crossOrigin = crossOrigin; + } + + // IE10 / IE11-Fix: SVG contents from data: URI + // will only be available if the IMG is present + // in the DOM (and visible) + if (url.substring(0,14) === 'data:image/svg') { + img.onload = null; + fabric.util.loadImageInDom(img, onLoadCallback); + } + + img.src = url; + }, + + /** + * Attaches SVG image with data: URL to the dom + * @memberOf fabric.util + * @param {Object} img Image object with data:image/svg src + * @param {Function} callback Callback; invoked with loaded image + * @return {Object} DOM element (div containing the SVG image) + */ + loadImageInDom: function(img, onLoadCallback) { + var div = fabric.document.createElement('div'); + div.style.width = div.style.height = '1px'; + div.style.left = div.style.top = '-100%'; + div.style.position = 'absolute'; + div.appendChild(img); + fabric.document.querySelector('body').appendChild(div); + /** + * Wrap in function to: + * 1. Call existing callback + * 2. Cleanup DOM + */ + img.onload = function () { + onLoadCallback(); + div.parentNode.removeChild(div); + div = null; + }; }, /** * Creates corresponding fabric instances from their object representations * @static * @memberOf fabric.util - * @param {Object[]} objects Objects to enliven + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created * @param {String} namespace Namespace to get klass "Class" object from * @param {Function} reviver Method for further parsing of object elements, * called after each fabric object created. */ - enlivenObjects: function(objects, namespace, reviver) { - return Promise.all(objects.map(function(obj) { - var klass = fabric.util.getKlass(obj.type, namespace); - return klass.fromObject(obj).then(function(fabricInstance) { - reviver && reviver(obj, fabricInstance); - return fabricInstance; + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || []; + + var enlivenedObjects = [], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects.filter(function(obj) { + // filter out undefined objects (objects that gave error) + return obj; + })); + } + } + + if (!numTotalObjects) { + callback && callback(enlivenedObjects); + return; + } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { + onLoaded(); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + klass.fromObject(o, function (obj, error) { + error || (enlivenedObjects[index] = obj); + reviver && reviver(o, obj, error); + onLoaded(); }); - })); + }); }, /** * Creates corresponding fabric instances residing in an object, e.g. `clipPath` - * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path ) - * @returns {Promise} the input object with enlived values + * @see {@link fabric.Object.ENLIVEN_PROPS} + * @param {Object} object + * @param {Object} [context] assign enlived props to this object (pass null to skip this) + * @param {(objects:fabric.Object[]) => void} callback + */ + enlivenObjectEnlivables: function (object, context, callback) { + var enlivenProps = fabric.Object.ENLIVEN_PROPS.filter(function (key) { return !!object[key]; }); + fabric.util.enlivenObjects(enlivenProps.map(function (key) { return object[key]; }), function (enlivedProps) { + var objects = {}; + enlivenProps.forEach(function (key, index) { + objects[key] = enlivedProps[index]; + context && (context[key] = enlivedProps[index]); + }); + callback && callback(objects); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} patterns Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * called after each fabric object created. */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; - enlivenObjectEnlivables: function (serializedObject) { - // enlive every possible property - var promises = Object.values(serializedObject).map(function(value) { - if (!value) { - return value; + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); } - if (value.colorStops) { - return new fabric.Gradient(value); - } - if (value.type) { - return fabric.util.enlivenObjects([value]).then(function (enlived) { - return enlived[0]; + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; + onLoaded(); }); } - if (value.source) { - return fabric.Pattern.fromObject(value); + else { + enlivenedPatterns[index] = p; + onLoaded(); } - return value; - }); - var keys = Object.keys(serializedObject); - return Promise.all(promises).then(function(enlived) { - return enlived.reduce(function(acc, instance, index) { - acc[keys[index]] = instance; - return acc; - }, {}); }); }, @@ -1240,7 +1304,7 @@ fabric.CommonMethods = { * @return {Array} properties Properties names to include */ populateWithProperties: function(source, destination, properties) { - if (properties && Array.isArray(properties)) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; @@ -1689,50 +1753,6 @@ fabric.CommonMethods = { object.setPositionByOrigin(center, 'center', 'center'); }, - /** - * - * A util that abstracts applying transform to objects.\ - * Sends `object` to the destination coordinate plane by applying the relevant transformations.\ - * Changes the space/plane where `object` is drawn.\ - * From the canvas/viewer's perspective `object` remains unchanged. - * - * @example Move clip path from one object to another while preserving it's appearance as viewed by canvas/viewer - * let obj, obj2; - * let clipPath = new fabric.Circle({ radius: 50 }); - * obj.clipPath = clipPath; - * // render - * fabric.util.sendObjectToPlane(clipPath, obj.calcTransformMatrix(), obj2.calcTransformMatrix()); - * obj.clipPath = undefined; - * obj2.clipPath = clipPath; - * // render, clipPath now clips obj2 but seems unchanged from the eyes of the viewer - * - * @example Clip an object's clip path with an existing object - * let obj, existingObj; - * let clipPath = new fabric.Circle({ radius: 50 }); - * obj.clipPath = clipPath; - * let transformTo = fabric.util.multiplyTransformMatrices(obj.calcTransformMatrix(), clipPath.calcTransformMatrix()); - * fabric.util.sendObjectToPlane(existingObj, existingObj.group?.calcTransformMatrix(), transformTo); - * clipPath.clipPath = existingObj; - * - * @static - * @memberof fabric.util - * @param {fabric.Object} object - * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `object` is a direct child of canvas. - * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `object` should be sent to the canvas coordinate plane. - * @returns {Matrix} the transform matrix that was applied to `object` - */ - sendObjectToPlane: function (object, from, to) { - // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) - // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) - var inv = fabric.util.invertTransform(to || fabric.iMatrix); - var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); - fabric.util.applyTransformToObject( - object, - fabric.util.multiplyTransformMatrices(t, object.calcOwnMatrix()) - ); - return t; - }, - /** * given a width and height, return the size of the bounding box * that can contains the box with width/height with applied transform @@ -2805,7 +2825,7 @@ fabric.CommonMethods = { /** * Creates an empty object and copies all enumerable properties of another object to it - * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. * @memberOf fabric.util.object * @param {Object} object Object to clone * @param {Boolean} [deep] Whether to clone nested objects @@ -2814,7 +2834,7 @@ fabric.CommonMethods = { //TODO: this function return an empty object if you try to clone null function clone(object, deep) { - return deep ? extend({ }, object, deep) : Object.assign({}, object); + return extend({ }, object, deep); } /** @namespace fabric.util.object */ @@ -3492,7 +3512,6 @@ fabric.CommonMethods = { /** * Cross-browser abstraction for sending XMLHttpRequest * @memberOf fabric.util - * @deprecated this has to go away, we can use a modern browser method to do the same. * @param {String} url URL to send XMLHttpRequest to * @param {Object} [options] Options object * @param {String} [options.method="GET"] @@ -4363,7 +4382,8 @@ fabric.warn = console.warn; } function normalizeValue(attr, value, parentAttributes, fontSize) { - var isArray = Array.isArray(value), parsed; + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; @@ -4761,7 +4781,7 @@ fabric.warn = console.warn; return; } - var xlink = xlinkAttribute.slice(1), + var xlink = xlinkAttribute.substr(1), x = el.getAttribute('x') || 0, y = el.getAttribute('y') || 0, el2 = elementById(doc, xlink).cloneNode(true), @@ -5033,7 +5053,7 @@ fabric.warn = console.warn; function recursivelyParseGradientsXlink(doc, gradient) { var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], xlinkAttr = 'xlink:href', - xLink = gradient.getAttribute(xlinkAttr).slice(1), + xLink = gradient.getAttribute(xlinkAttr).substr(1), referencedGradient = elementById(doc, xLink); if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { recursivelyParseGradientsXlink(doc, referencedGradient); @@ -5648,61 +5668,47 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, - /** - * Multiplies this point by another value and returns a new one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - multiply: function (that) { - return new Point(this.x * that.x, this.y * that.y); - }, - /** * Multiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - scalarMultiply: function (scalar) { + multiply: function (scalar) { return new Point(this.x * scalar, this.y * scalar); }, /** * Multiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - scalarMultiplyEquals: function (scalar) { + multiplyEquals: function (scalar) { this.x *= scalar; this.y *= scalar; return this; }, - /** - * Divides this point by another and returns a new one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - divide: function (that) { - return new Point(this.x / that.x, this.y / that.y); - }, - /** * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - scalarDivide: function (scalar) { + divide: function (scalar) { return new Point(this.x / scalar, this.y / scalar); }, /** * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - scalarDivideEquals: function (scalar) { + divideEquals: function (scalar) { this.x /= scalar; this.y /= scalar; return this; @@ -6720,9 +6726,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @return {Number} 0 - 7 a quadrant number */ function findCornerQuadrant(fabricObject, control) { - // angle is relative to canvas plane - var angle = fabricObject.getTotalAngle(); - var cornerAngle = angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; return Math.round((cornerAngle % 360) / 45); } @@ -6891,7 +6895,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ function wrapWithFixedAnchor(actionHandler) { return function(eventData, transform, x, y) { - var target = transform.target, centerPoint = target.getRelativeCenterPoint(), + var target = transform.target, centerPoint = target.getCenterPoint(), constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), actionPerformed = actionHandler(eventData, transform, x, y); target.setPositionByOrigin(constraint, transform.originX, transform.originY); @@ -6929,7 +6933,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp control = target.controls[transform.corner], zoom = target.canvas.getZoom(), padding = target.padding / zoom, - localPoint = target.normalizePoint(new fabric.Point(x, y), originX, originY); + localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); if (localPoint.x >= padding) { localPoint.x -= padding; } @@ -6975,7 +6979,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectX(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions({ skewX: 0, skewY: target.skewY }), + dimNoSkew = target._getTransformedDimensions(0, target.skewY), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7018,7 +7022,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectY(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions({ skewX: target.skewX, skewY: 0 }), + dimNoSkew = target._getTransformedDimensions(target.skewX, 0), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7167,7 +7171,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function rotationWithSnapping(eventData, transform, x, y) { var t = transform, target = t.target, - pivotPoint = target.translateToOriginPoint(target.getRelativeCenterPoint(), t.originX, t.originY); + pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); if (target.lockRotation) { return false; @@ -7522,9 +7526,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp // this is still wrong ctx.lineWidth = 1; ctx.translate(left, top); - // angle is relative to canvas plane - var angle = fabricObject.getTotalAngle(); - ctx.rotate(degreesToRadians(angle)); + ctx.rotate(degreesToRadians(fabricObject.angle)); // this does not work, and fixed with ( && ) does not make sense. // to have real transparent corners we need the controls on upperCanvas // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); @@ -8427,18 +8429,30 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ patternTransform: null, - type: 'pattern', - /** * Constructor * @param {Object} [options] Options object - * @param {option.source} [source] the pattern source, eventually empty or a drawable + * @param {Function} [callback] function to invoke after callback init. * @return {fabric.Pattern} thisArg */ - initialize: function(options) { + initialize: function(options, callback) { options || (options = { }); + this.id = fabric.Object.__uid++; this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img, isError) { + _this.source = img; + callback && callback(_this, isError); + }, null, this.crossOrigin); + } }, /** @@ -8550,15 +8564,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return ctx.createPattern(source, this.repeat); } }); - - fabric.Pattern.fromObject = function(object) { - var patternOptions = Object.assign({}, object); - return fabric.util.loadImage(object.source, { crossOrigin: object.crossOrigin }) - .then(function(img) { - patternOptions.source = img; - return new fabric.Pattern(patternOptions); - }); - }; })(); @@ -8810,6 +8815,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. * @type {(String|fabric.Pattern)} * @default */ @@ -8827,6 +8833,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} * @since 1.3.9 * @type {(String|fabric.Pattern)} * @default @@ -8963,6 +8970,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @param {Object} [options] Options object */ _initStatic: function(el, options) { + var cb = this.requestRenderAllBound; this._objects = []; this._createLowerCanvas(el); this._initOptions(options); @@ -8970,6 +8978,19 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp if (!this.interactive) { this._initRetinaScaling(); } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } this.calcOffset(); }, @@ -9020,6 +9041,202 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, + /** + * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + // TODO: fix stretched examples + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to + * @param {Function} callback Callback to invoke when foreground color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img, isError) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img, isError); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + image && (image.canvas = this); + callback && callback(image, false); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + /** * @private */ @@ -9576,7 +9793,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * Returns coordinates of a center of canvas. * Returned value is an object with top and left properties * @return {Object} object with "top" and "left" number values - * @deprecated migrate to `getCenterPoint` */ getCenter: function () { return { @@ -9585,21 +9801,13 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }; }, - /** - * Returns coordinates of a center of canvas. - * @return {fabric.Point} - */ - getCenterPoint: function () { - return new fabric.Point(this.width / 2, this.height / 2); - }, - /** * Centers object horizontally in the canvas * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - return this._centerObject(object, new fabric.Point(this.getCenterPoint().x, object.getCenterPoint().y)); + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); }, /** @@ -9609,7 +9817,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObjectV: function (object) { - return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenterPoint().y)); + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); }, /** @@ -9619,8 +9827,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObject: function(object) { - var center = this.getCenterPoint(); - return this._centerObject(object, center); + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); }, /** @@ -9631,6 +9840,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ viewportCenterObject: function(object) { var vpCenter = this.getVpCenter(); + return this._centerObject(object, vpCenter); }, @@ -9664,9 +9874,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ getVpCenter: function() { - var center = this.getCenterPoint(), + var center = this.getCenter(), iVpt = invertTransform(this.viewportTransform); - return transformPoint(center, iVpt); + return transformPoint({ x: center.left, y: center.top }, iVpt); }, /** @@ -11668,13 +11878,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _hoveredTargets: [], - /** - * hold the list of objects to render - * @type fabric.Object[] - * @private - */ - _objectsToRender: undefined, - /** * @private */ @@ -11692,23 +11895,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); }, - /** - * @private - * @param {fabric.Object} obj Object that was added - */ - _onObjectAdded: function (obj) { - this._objectsToRender = undefined; - this.callSuper('_onObjectAdded', obj); - }, - - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function (obj) { - this._objectsToRender = undefined; - this.callSuper('_onObjectRemoved', obj); - }, /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. @@ -11718,7 +11904,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var activeObjects = this.getActiveObjects(), object, objsToRender, activeGroupObjects; - if (!this.preserveObjectStacking && activeObjects.length > 1) { + if (activeObjects.length > 0 && !this.preserveObjectStacking) { objsToRender = []; activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { @@ -11735,15 +11921,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } objsToRender.push.apply(objsToRender, activeGroupObjects); } - // in case a single object is selected render it's entire above the other objects - else if (!this.preserveObjectStacking && activeObjects.length === 1) { - var target = activeObjects[0], ancestors = target.getAncestors(true); - var topAncestor = ancestors.length === 0 ? target : ancestors.pop(); - objsToRender = this._objects.slice(); - var index = objsToRender.indexOf(topAncestor); - index > -1 && objsToRender.splice(objsToRender.indexOf(topAncestor), 1); - objsToRender.push(topAncestor); - } else { objsToRender = this._objects; } @@ -11765,8 +11942,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.hasLostContext = false; } var canvasToDrawOn = this.contextContainer; - !this._objectsToRender && (this._objectsToRender = this._chooseObjectsToRender()); - this.renderCanvas(canvasToDrawOn, this._objectsToRender); + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); return this; }, @@ -11857,7 +12033,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _isSelectionKeyPressed: function(e) { var selectionKeyPressed = false; - if (Array.isArray(this.selectionKey)) { + if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); } else { @@ -11972,22 +12148,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (!target) { return; } - var pointer = this.getPointer(e); - if (target.group) { - // transform pointer to target's containing coordinate plane - pointer = fabric.util.transformPoint(pointer, fabric.util.invertTransform(target.group.calcTransformMatrix())); - } - var corner = target.__corner, + + var pointer = this.getPointer(e), corner = target.__corner, control = target.controls[corner], actionHandler = (alreadySelected && corner) ? control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, action = this._getActionFromCorner(alreadySelected, corner, e, target), origin = this._getOriginFromCorner(target, corner), altKey = e[this.centeredKey], - /** - * relative to target's containing coordinate plane - * both agree on every point - **/ transform = { target: target, action: action, @@ -11997,6 +12165,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, + // used by transation offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, @@ -12005,7 +12174,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ey: pointer.y, lastX: pointer.x, lastY: pointer.y, + // unsure they are useful anymore. + // left: target.left, + // top: target.top, theta: degreesToRadians(target.angle), + // end of unsure width: target.width * target.scaleX, shiftKey: e.shiftKey, altKey: altKey, @@ -12098,12 +12271,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { return activeObject; } - if (aObjects.length > 1 && activeObject.type === 'activeSelection' - && !skipGroup && this.searchPossibleTargets([activeObject], pointer)) { + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { return activeObject; } if (aObjects.length === 1 && - activeObject === this.searchPossibleTargets([activeObject], pointer)) { + activeObject === this._searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { return activeObject; } @@ -12113,7 +12285,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.targets = []; } } - var target = this.searchPossibleTargets(this._objects, pointer); + var target = this._searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; this.targets = activeTargetSubs; @@ -12150,10 +12322,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Internal Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @param {Array} [objects] objects array to look into * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} **top most object from given `objects`** that contains pointer + * @return {fabric.Object} object that contains pointer * @private */ _searchPossibleTargets: function(objects, pointer) { @@ -12177,18 +12349,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return target; }, - /** - * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted - * @see {@link fabric.Canvas#_searchPossibleTargets} - * @param {Array} [objects] objects array to look into - * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} **top most object on screen** that contains pointer - */ - searchPossibleTargets: function (objects, pointer) { - var target = this._searchPossibleTargets(objects, pointer); - return target; - }, - /** * Returns pointer coordinates without the effect of the viewport * @param {Object} pointer with "x" and "y" number values @@ -12204,27 +12364,27 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * Returns pointer coordinates relative to canvas. * Can return coordinates with or without viewportTransform. - * ignoreVpt false gives back coordinates that represent + * ignoreZoom false gives back coordinates that represent * the point clicked on canvas element. - * ignoreVpt true gives back coordinates after being processed + * ignoreZoom true gives back coordinates after being processed * by the viewportTransform ( sort of coordinates of what is displayed * on the canvas where you are clicking. - * ignoreVpt true = HTMLElement coordinates relative to top,left - * ignoreVpt false, default = fabric space coordinates, the same used for shape position - * To interact with your shapes top and left you want to use ignoreVpt true - * most of the time, while ignoreVpt false will give you coordinates + * ignoreZoom true = HTMLElement coordinates relative to top,left + * ignoreZoom false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates * compatible with the object.oCoords system. * of the time. * @param {Event} e - * @param {Boolean} ignoreVpt + * @param {Boolean} ignoreZoom * @return {Object} object with "x" and "y" number values */ - getPointer: function (e, ignoreVpt) { + getPointer: function (e, ignoreZoom) { // return cached values if we are in the event processing chain - if (this._absolutePointer && !ignoreVpt) { + if (this._absolutePointer && !ignoreZoom) { return this._absolutePointer; } - if (this._pointer && ignoreVpt) { + if (this._pointer && ignoreZoom) { return this._pointer; } @@ -12247,7 +12407,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; - if (!ignoreVpt) { + if (!ignoreZoom) { pointer = this.restorePointerVpt(pointer); } @@ -12299,14 +12459,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.contextTop = upperCanvasEl.getContext('2d'); }, - /** - * Returns context of top canvas where interactions are drawn - * @returns {CanvasRenderingContext2D} - */ - getTopContext: function () { - return this.contextTop; - }, - /** * @private */ @@ -12431,7 +12583,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _fireSelectionEvents: function(oldObjects, e) { var somethingChanged = false, objects = this.getActiveObjects(), - added = [], removed = [], invalidate = false; + added = [], removed = []; oldObjects.forEach(function(oldObject) { if (objects.indexOf(oldObject) === -1) { somethingChanged = true; @@ -12453,7 +12605,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } }); if (oldObjects.length > 0 && objects.length > 0) { - invalidate = true; somethingChanged && this.fire('selection:updated', { e: e, selected: added, @@ -12461,20 +12612,17 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }); } else if (objects.length > 0) { - invalidate = true; this.fire('selection:created', { e: e, selected: added, }); } else if (oldObjects.length > 0) { - invalidate = true; this.fire('selection:cleared', { e: e, deselected: removed, }); } - invalidate && (this._objectsToRender = undefined); }, /** @@ -13859,9 +14007,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 - * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - * @see {@link https://jsfiddle.net/xsjua1rd/ demo} + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} * @example Generate jpeg dataURL with lower quality * var dataURL = canvas.toDataURL({ * format: 'jpeg', @@ -13880,11 +14027,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * format: 'png', * multiplier: 2 * }); - * @example Generate dataURL with objects that overlap a specified object - * var myObject; - * var dataURL = canvas.toDataURL({ - * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject) - * }); */ toDataURL: function (options) { options || (options = { }); @@ -13903,31 +14045,29 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * This is an intermediary step used to get to a dataUrl but also it is useful to * create quick image copies of a canvas without passing for the dataUrl string * @param {Number} [multiplier] a zoom factor. - * @param {Object} [options] Cropping informations - * @param {Number} [options.left] Cropping left offset. - * @param {Number} [options.top] Cropping top offset. - * @param {Number} [options.width] Cropping width. - * @param {Number} [options.height] Cropping height. - * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. - */ - toCanvasElement: function (multiplier, options) { + * @param {Object} [cropping] Cropping informations + * @param {Number} [cropping.left] Cropping left offset. + * @param {Number} [cropping.top] Cropping top offset. + * @param {Number} [cropping.width] Cropping width. + * @param {Number} [cropping.height] Cropping height. + */ + toCanvasElement: function(multiplier, cropping) { multiplier = multiplier || 1; - options = options || { }; - var scaledWidth = (options.width || this.width) * multiplier, - scaledHeight = (options.height || this.height) * multiplier, + cropping = cropping || { }; + var scaledWidth = (cropping.width || this.width) * multiplier, + scaledHeight = (cropping.height || this.height) * multiplier, zoom = this.getZoom(), originalWidth = this.width, originalHeight = this.height, newZoom = zoom * multiplier, vp = this.viewportTransform, - translateX = (vp[4] - (options.left || 0)) * multiplier, - translateY = (vp[5] - (options.top || 0)) * multiplier, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, originalInteractive = this.interactive, newVp = [newZoom, 0, 0, newZoom, translateX, translateY], originalRetina = this.enableRetinaScaling, canvasEl = fabric.util.createCanvasElement(), - originalContextTop = this.contextTop, - objectsToRender = options.filter ? this._objects.filter(options.filter) : this._objects; + originalContextTop = this.contextTop; canvasEl.width = scaledWidth; canvasEl.height = scaledHeight; this.contextTop = null; @@ -13937,7 +14077,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.width = scaledWidth; this.height = scaledHeight; this.calcViewportBoundaries(); - this.renderCanvas(canvasEl.getContext('2d'), objectsToRender); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); this.viewportTransform = vp; this.width = originalWidth; this.height = originalHeight; @@ -13957,23 +14097,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. - * @return {Promise} instance + * @return {fabric.Canvas} instance * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} * @example loadFromJSON - * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll()); + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); * @example loadFromJSON with reviver - * canvas.loadFromJSON(json, function(o, object) { + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { * // `o` = json object * // `object` = fabric.Object instance * // ... do some stuff ... - * }).then((canvas) => { - * ... canvas is restored, add your code. * }); */ - loadFromJSON: function (json, reviver) { + loadFromJSON: function (json, callback, reviver) { if (!json) { return; } @@ -13984,35 +14125,38 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati : fabric.util.object.clone(json); var _this = this, + clipPath = serialized.clipPath, renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; - return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) - .then(function(enlived) { - _this.clear(); - return fabric.util.enlivenObjectEnlivables({ - backgroundImage: serialized.backgroundImage, - backgroundColor: serialized.background, - overlayImage: serialized.overlayImage, - overlayColor: serialized.overlay, - clipPath: serialized.clipPath, - }) - .then(function(enlivedMap) { - _this.__setupCanvas(serialized, enlived, renderOnAddRemove); - _this.set(enlivedMap); - return _this; + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); + _this._setBgOverlay(serialized, function () { + if (clipPath) { + _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { + _this.clipPath = enlivenedCanvasClip[0]; + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); }); + } + else { + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + } }); + }, reviver); + return this; }, /** * @private * @param {Object} serialized Object with background and overlay information - * @param {Array} enlivenedObjects canvas objects - * @param {boolean} renderOnAddRemove renderOnAddRemove setting for the canvas + * @param {Array} restored canvas objects + * @param {Function} cached renderOnAddRemove callback + * @param {Function} callback Invoked after all background and overlay images/patterns loaded */ - __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove) { + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { var _this = this; enlivenedObjects.forEach(function(obj, index) { // we splice the array just in case some custom classes restored from JSON @@ -14031,41 +14175,149 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // create the Object instance. Here the Canvas is // already an instance and we are just loading things over it this._setOptions(serialized); + this.renderAll(); + callback && callback(); }, /** - * Clones canvas instance - * @param {Array} [properties] Array of properties to include in the cloned canvas and children - * @returns {Promise} - */ - clone: function (properties) { - var data = JSON.stringify(this.toJSON(properties)); - return this.cloneWithoutData().then(function(clone) { - return clone.loadFromJSON(data); - }); + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); }, /** - * Clones canvas instance without cloning existing data. - * This essentially copies canvas dimensions, clipping properties, etc. - * but leaves data empty (so that you can populate it with your own) - * @returns {Promise} + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set */ - cloneWithoutData: function() { - var el = fabric.util.createCanvasElement(); + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + if (!objects || objects.length === 0) { + callback && callback([]); + return; + } + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + */ + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.util.createCanvasElement(); el.width = this.width; el.height = this.height; - // this seems wrong. either Canvas or StaticCanvas + var clone = new fabric.Canvas(el); - var data = {}; if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.toObject(); + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; } - if (this.backgroundColor) { - data.background = this.backgroundColor.toObject ? this.backgroundColor.toObject() : this.backgroundColor; + else { + callback && callback(clone); } - return clone.loadFromJSON(data); } }); @@ -14799,17 +15051,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _getCacheCanvasDimensions: function() { var objectScale = this.getTotalObjectScaling(), // caculate dimensions without skewing - dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), - neededX = dim.x * objectScale.x / this.scaleX, - neededY = dim.y * objectScale.y / this.scaleY; + dim = this._getTransformedDimensions(0, 0), + neededX = dim.x * objectScale.scaleX / this.scaleX, + neededY = dim.y * objectScale.scaleY / this.scaleY; return { // for sure this ALIASING_LIMIT is slightly creating problem // in situation in which the cache canvas gets an upper limit // also objectScale contains already scaleX and scaleY width: neededX + ALIASING_LIMIT, height: neededY + ALIASING_LIMIT, - zoomX: objectScale.x, - zoomY: objectScale.y, + zoomX: objectScale.scaleX, + zoomY: objectScale.scaleY, x: neededX, y: neededY }; @@ -14887,6 +15139,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ setOptions: function(options) { this._setOptions(options); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); }, /** @@ -14980,9 +15236,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (object[prop] === prototype[prop]) { delete object[prop]; } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + // basically a check for [] === [] - if (Array.isArray(object[prop]) && Array.isArray(prototype[prop]) - && object[prop].length === 0 && prototype[prop].length === 0) { + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { delete object[prop]; } }); @@ -15000,7 +15258,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Return the object scale factor counting also the group scaling - * @return {fabric.Point} + * @return {Object} object with scaleX and scaleY properties */ getObjectScaling: function() { // if the object is a top level one, on the canvas, we go for simple aritmetic @@ -15008,11 +15266,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // and will likely kill the cache when not needed // https://github.com/fabricjs/fabric.js/issues/7157 if (!this.group) { - return new fabric.Point(Math.abs(this.scaleX), Math.abs(this.scaleY)); + return { + scaleX: this.scaleX, + scaleY: this.scaleY, + }; } // if we are inside a group total zoom calculation is complex, we defer to generic matrices var options = fabric.util.qrDecompose(this.calcTransformMatrix()); - return new fabric.Point(Math.abs(options.scaleX), Math.abs(options.scaleY)); + return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; }, /** @@ -15020,13 +15281,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Object} object with scaleX and scaleY properties */ getTotalObjectScaling: function() { - var scale = this.getObjectScaling(); + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; if (this.canvas) { var zoom = this.canvas.getZoom(); var retina = this.canvas.getRetinaScaling(); - scale.scalarMultiplyEquals(zoom * retina); + scaleX *= zoom * retina; + scaleY *= zoom * retina; } - return scale; + return { scaleX: scaleX, scaleY: scaleY }; }, /** @@ -15041,16 +15303,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return opacity; }, - /** - * Returns the object angle relative to canvas counting also the group property - * @returns {number} - */ - getTotalAngle: function () { - return this.group ? - fabric.util.qrDecompose(this.calcTransformMatrix()).angle : - this.angle; - }, - /** * @private * @param {String} key @@ -15094,6 +15346,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this; }, + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform @@ -15154,7 +15416,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati renderCache: function(options) { options = options || {}; - if (!this._cacheCanvas || !this._cacheContext) { + if (!this._cacheCanvas) { this._createCacheCanvas(); } if (this.isCacheDirty()) { @@ -15169,7 +15431,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _removeCacheCanvas: function() { this._cacheCanvas = null; - this._cacheContext = null; this.cacheWidth = 0; this.cacheHeight = 0; }, @@ -15242,7 +15503,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Check if this object or a child object will cast a shadow * used by Group.shouldCache to know if child has a shadow recursively * @return {Boolean} - * @deprecated */ willDrawShadow: function() { return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); @@ -15329,7 +15589,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (this.isNotVisible()) { return false; } - if (this._cacheCanvas && this._cacheContext && !skipCanvas && this._updateCacheCanvas()) { + if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { // in this case the context is already cleared. return true; } @@ -15338,7 +15598,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati (this.clipPath && this.clipPath.absolutePositioned) || (this.statefullCache && this.hasStateChanged('cacheProperties')) ) { - if (this._cacheCanvas && this._cacheContext && !skipCanvas) { + if (this._cacheCanvas && !skipCanvas) { var width = this.cacheWidth / this.zoomX; var height = this.cacheHeight / this.zoomY; this._cacheContext.clearRect(-width / 2, -height / 2, width, height); @@ -15494,19 +15754,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return; } - var shadow = this.shadow, canvas = this.canvas, + var shadow = this.shadow, canvas = this.canvas, scaling, multX = (canvas && canvas.viewportTransform[0]) || 1, - multY = (canvas && canvas.viewportTransform[3]) || 1, - scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); + multY = (canvas && canvas.viewportTransform[3]) || 1; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } if (canvas && canvas._isRetinaScaling()) { multX *= fabric.devicePixelRatio; multY *= fabric.devicePixelRatio; } ctx.shadowColor = shadow.color; ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * - (multX + multY) * (scaling.x + scaling.y) / 4; - ctx.shadowOffsetX = shadow.offsetX * multX * scaling.x; - ctx.shadowOffsetY = shadow.offsetY * multY * scaling.y; + (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; }, /** @@ -15609,9 +15874,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati } ctx.save(); - if (this.strokeUniform) { + if (this.strokeUniform && this.group) { var scaling = this.getObjectScaling(); - ctx.scale(1 / scaling.x, 1 / scaling.y); + ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); + } + else if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); } this._setLineDash(ctx, this.strokeDashArray); this._setStrokeStyles(ctx, this); @@ -15713,13 +15981,18 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Clones an instance. + * Clones an instance, using a callback method will work for every object. + * @param {Function} callback Callback is invoked with a clone as a first argument * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @returns {Promise} */ - clone: function(propertiesToInclude) { + clone: function(callback, propertiesToInclude) { var objectForm = this.toObject(propertiesToInclude); - return this.constructor.fromObject(objectForm); + if (this.constructor.fromObject) { + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); + } }, /** @@ -15729,6 +16002,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * and format option. toCanvasElement is faster and produce no loss of quality. * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. * toCanvasElement and then toBlob from the obtained canvas is also a good option. + * This method is sync now, but still support the callback because we did not want to break. + * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. + * @param {Function} callback callback, invoked with an instance as a first argument * @param {Object} [options] for clone as image, passed to toDataURL * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 @@ -15738,11 +16014,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {fabric.Image} Object cloned as image. + * @return {fabric.Object} thisArg */ - cloneAsImage: function(options) { + cloneAsImage: function(callback, options) { var canvasEl = this.toCanvasElement(options); - return new fabric.Image(canvasEl); + if (callback) { + callback(new fabric.Image(canvasEl)); + } + return this; }, /** @@ -15776,15 +16055,21 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var el = fabric.util.createCanvasElement(), // skip canvas zoom and calculate with setCoords now. boundingRect = this.getBoundingRect(true, true), - shadow = this.shadow, shadowOffset = { x: 0, y: 0 }, + shadow = this.shadow, scaling, + shadowOffset = { x: 0, y: 0 }, shadowBlur, width, height; if (shadow) { - var shadowBlur = shadow.blur; - var scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } // consider non scaling shadow. - shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.x)); - shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.y)); + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); } width = boundingRect.width + shadowOffset.x; height = boundingRect.height + shadowOffset.y; @@ -15847,7 +16132,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} */ isType: function(type) { - return arguments.length > 1 ? Array.from(arguments).includes(this.type) : this.type === type; + return this.type === type; }, /** @@ -15957,13 +16242,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * This callback function is called by the parent group of an object every - * time a non-delegated property changes on the group. It is passed the key - * and value as parameters. Not adding in this function's signature to avoid - * Travis build error about unused variables. + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) */ - setOnGroup: function() { - // implemented by sub-classes, as needed. + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; }, /** @@ -16009,19 +16304,25 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @constant * @type string[] */ + fabric.Object.ENLIVEN_PROPS = ['clipPath']; - fabric.Object._fromObject = function(klass, object, extraParam) { - var serializedObject = clone(object, true); - return fabric.util.enlivenObjectEnlivables(serializedObject).then(function(enlivedMap) { - var newObject = Object.assign(object, enlivedMap); - return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject); + fabric.Object._fromObject = function(className, object, callback, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + fabric.util.enlivenObjectEnlivables(object, object, function () { + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); }); }; - fabric.Object.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Object, object); - }; - /** * Unique id used internally when creating SVG elements * @static @@ -16046,52 +16347,53 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati bottom: 0.5 }; - /** - * @typedef {number | 'left' | 'center' | 'right'} OriginX - * @typedef {number | 'top' | 'center' | 'bottom'} OriginY - */ - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - /** - * Resolves origin value relative to center - * @private - * @param {OriginX} originX - * @returns number - */ - resolveOriginX: function (originX) { - return typeof originX === 'string' ? - originXOffset[originX] : - originX - 0.5; - }, - - /** - * Resolves origin value relative to center - * @private - * @param {OriginY} originY - * @returns number - */ - resolveOriginY: function (originY) { - return typeof originY === 'string' ? - originYOffset[originY] : - originY - 0.5; - }, - /** * Translates the coordinates from a set of origin to another (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {OriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {OriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { var x = point.x, y = point.y, - dim, - offsetX = this.resolveOriginX(toOriginX) - this.resolveOriginX(fromOriginX), - offsetY = this.resolveOriginY(toOriginY) - this.resolveOriginY(fromOriginY); + offsetX, offsetY, dim; + + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; + } + + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } + + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; if (offsetX || offsetY) { dim = this._getTransformedDimensions(); @@ -16105,8 +16407,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Translates the coordinates from origin to center coordinates (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { @@ -16120,8 +16422,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Translates the coordinates from center to origin coordinates (based on the object's dimensions) * @param {fabric.Point} center The point which corresponds to center of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { @@ -16133,30 +16435,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Returns the center coordinates of the object relative to canvas + * Returns the real center coordinates of the object * @return {fabric.Point} */ getCenterPoint: function() { - var relCenter = this.getRelativeCenterPoint(); - return this.group ? - fabric.util.transformPoint(relCenter, this.group.calcTransformMatrix()) : - relCenter; - }, - - /** - * Returns the center coordinates of the object relative to it's containing group or null - * @return {fabric.Point|null} point or null of object has no parent group - */ - getCenterPointRelativeToParent: function () { - return this.group ? this.getRelativeCenterPoint() : null; - }, - - /** - * Returns the center coordinates of the object relative to it's parent - * @return {fabric.Point} - */ - getRelativeCenterPoint: function () { - return this.translateToCenterPoint(new fabric.Point(this.left, this.top), this.originX, this.originY); + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); }, /** @@ -16170,24 +16454,26 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Returns the coordinates of the object as if it has a different origin - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ getPointByOrigin: function(originX, originY) { - var center = this.getRelativeCenterPoint(); + var center = this.getCenterPoint(); return this.translateToOriginPoint(center, originX, originY); }, /** - * Returns the normalized point (rotated relative to center) in local coordinates - * @param {fabric.Point} point The point relative to instance coordinate system - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * Returns the point in local coordinates + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ - normalizePoint: function(point, originX, originY) { - var center = this.getRelativeCenterPoint(), p, p2; + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); } @@ -16202,20 +16488,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return p2.subtractEquals(p); }, - /** - * Returns coordinates of a pointer relative to object's top left corner in object's plane - * @param {Event} e Event to operate upon - * @param {Object} [pointer] Pointer to operate upon (instead of event) - * @return {Object} Coordinates of a pointer (x, y) - */ - getLocalPointer: function (e, pointer) { - pointer = pointer || this.canvas.getPointer(e); - return fabric.util.transformPoint( - new fabric.Point(pointer.x, pointer.y), - fabric.util.invertTransform(this.calcTransformMatrix()) - ).addEquals(new fabric.Point(this.width / 2, this.height / 2)); - }, - /** * Returns the point in global coordinates * @param {fabric.Point} The point relative to the local coordinate system @@ -16228,8 +16500,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Sets the position of the object taking into consideration the object's origin * @param {fabric.Point} pos The new position of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {void} */ setPositionByOrigin: function(pos, originX, originY) { @@ -16277,7 +16549,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this._originalOriginX = this.originX; this._originalOriginY = this.originY; - var center = this.getRelativeCenterPoint(); + var center = this.getCenterPoint(); this.originX = 'center'; this.originY = 'center'; @@ -16293,7 +16565,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _resetOrigin: function() { var originPoint = this.translateToOriginPoint( - this.getRelativeCenterPoint(), + this.getCenterPoint(), this._originalOriginX, this._originalOriginY); @@ -16311,7 +16583,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @private */ _getLeftTopCoords: function() { - return this.translateToOriginPoint(this.getRelativeCenterPoint(), 'left', 'top'); + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); }, }); @@ -16751,7 +17023,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati calcLineCoords: function() { var vpt = this.getViewportTransform(), - padding = this.padding, angle = degreesToRadians(this.getTotalAngle()), + padding = this.padding, angle = degreesToRadians(this.angle), cos = util.cos(angle), sin = util.sin(angle), cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); @@ -16781,8 +17053,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var rotateMatrix = this._calcRotateMatrix(), translateMatrix = this._calcTranslateMatrix(), vpt = this.getViewportTransform(), - startMatrix = this.group ? multiplyMatrices(vpt, this.group.calcTransformMatrix()) : vpt, - startMatrix = multiplyMatrices(startMatrix, translateMatrix), + startMatrix = multiplyMatrices(vpt, translateMatrix), finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), dim = this._calculateCurrentDimensions(), @@ -16792,18 +17063,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }); // debug code - /* - var canvas = this.canvas; - setTimeout(function () { - if (!canvas) return; - canvas.contextTop.clearRect(0, 0, 700, 700); - canvas.contextTop.fillStyle = 'green'; - Object.keys(coords).forEach(function(key) { - var control = coords[key]; - canvas.contextTop.fillRect(control.x, control.y, 3, 3); - }); - }, 50); - */ + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // Object.keys(coords).forEach(function(key) { + // var control = coords[key]; + // canvas.contextTop.fillRect(control.x, control.y, 3, 3); + // }); + // }, 50); return coords; }, @@ -16860,7 +17128,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Array} rotation matrix for the object */ _calcTranslateMatrix: function() { - var center = this.getRelativeCenterPoint(); + var center = this.getCenterPoint(); return [1, 0, 0, 1, center.x, center.y]; }, @@ -16925,62 +17193,77 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return cache.value; }, - /** + /* * Calculate object dimensions from its properties * @private - * @returns {fabric.Point} dimensions + * @return {Object} .x width dimension + * @return {Object} .y height dimension */ _getNonTransformedDimensions: function() { - return new fabric.Point(this.width, this.height).scalarAddEquals(this.strokeWidth); + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; }, - /** + /* * Calculate object bounding box dimensions from its properties scale, skew. - * @param {Object} [options] - * @param {Number} [options.scaleX] - * @param {Number} [options.scaleY] - * @param {Number} [options.skewX] - * @param {Number} [options.skewY] + * @param {Number} skewX, a value to override current skewX + * @param {Number} skewY, a value to override current skewY * @private - * @returns {fabric.Point} dimensions + * @return {Object} .x width dimension + * @return {Object} .y height dimension */ - _getTransformedDimensions: function (options) { - options = Object.assign({ - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - }, options || {}); - // stroke is applied before/after transformations are applied according to `strokeUniform` - var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = this.strokeWidth; + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions, dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + if (this.strokeUniform) { - preScalingStrokeValue = 0; - postScalingStrokeValue = strokeWidth; + dimX = this.width; + dimY = this.height; } else { - preScalingStrokeValue = strokeWidth; - postScalingStrokeValue = 0; + dimensions = this._getNonTransformedDimensions(); + dimX = dimensions.x; + dimY = dimensions.y; } - var dimX = this.width + preScalingStrokeValue, - dimY = this.height + preScalingStrokeValue, - finalDimensions, - noSkew = options.skewX === 0 && options.skewY === 0; if (noSkew) { - finalDimensions = new fabric.Point(dimX * options.scaleX, dimY * options.scaleY); - } - else { - var bbox = util.sizeAfterTransform(dimX, dimY, options); - finalDimensions = new fabric.Point(bbox.x, bbox.y); + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); } + var bbox = util.sizeAfterTransform(dimX, dimY, { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: skewX, + skewY: skewY, + }); + return this._finalizeDimensions(bbox.x, bbox.y); + }, - return finalDimensions.scalarAddEquals(postScalingStrokeValue); + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param Number width width of the bbox + * @param Number height height of the bbox + * @private + * @return {Object} .x finalized width dimension + * @return {Object} .y finalized height dimension + */ + _finalizeDimensions: function(width, height) { + return this.strokeUniform ? + { x: width + this.strokeWidth, y: height + this.strokeWidth } + : + { x: width, y: height }; }, - /** + /* * Calculate object dimensions for controls box, including padding and canvas zoom. * and active selection - * @private - * @returns {fabric.Point} dimensions + * private */ _calculateCurrentDimensions: function() { var vpt = this.getViewportTransform(), @@ -16992,25 +17275,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati })(); -fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * - * @param {boolean} [strict] returns only ancestors that are objects (without canvas) - * @returns {(fabric.Object | fabric.StaticCanvas)[]} ancestors from bottom to top - */ - getAncestors: function (strict) { - var ancestors = []; - var parent = this.group || (!strict ? this.canvas : undefined); - while (parent) { - ancestors.push(parent); - parent = parent.group || (!strict ? parent.canvas : undefined); - } - return ancestors; - } -}); - - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** @@ -17474,10 +17738,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(pointer, forTouch) { - if (!this.hasControls || (!this.canvas || this.canvas._activeObject !== this)) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { return false; } - var xPoints, + + var ex = pointer.x, + ey = pointer.y, + xPoints, lines, keys = Object.keys(this.oCoords), j = keys.length - 1, i; this.__corner = 0; @@ -17504,7 +17773,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - xPoints = this._findCrossPoints(pointer, lines); + xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); if (xPoints !== 0 && xPoints % 2 === 1) { this.__corner = i; return i; @@ -17560,7 +17829,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; } ctx.save(); - var center = this.getRelativeCenterPoint(), wh = this._calculateCurrentDimensions(), + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), vpt = this.canvas.viewportTransform; ctx.translate(center.x, center.y); ctx.scale(1 / vpt[0], 1 / vpt[3]); @@ -17587,7 +17856,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = wh.x + strokeWidth, height = wh.y + strokeWidth, hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls; + styleOverride.hasControls : this.hasControls, + shouldStroke = false; ctx.save(); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17599,8 +17869,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); - hasControls && this.drawControlsConnectingLines(ctx); + if (hasControls) { + ctx.beginPath(); + this.forEachControl(function(control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + if (shouldStroke) { + ctx.stroke(); + } + } ctx.restore(); return this; }, @@ -17624,9 +17912,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, height = - bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor, - hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls; + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; ctx.save(); this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17636,46 +17922,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); - hasControls && this.drawControlsConnectingLines(ctx); ctx.restore(); return this; }, - /** - * Draws lines from a borders of an object's bounding box to controls that have `withConnection` property set. - * Requires public properties: width, height - * Requires public options: padding, borderColor - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @return {fabric.Object} thisArg - * @chainable - */ - drawControlsConnectingLines: function (ctx) { - var wh = this._calculateCurrentDimensions(), - strokeWidth = this.borderScaleFactor, - width = wh.x + strokeWidth, - height = wh.y + strokeWidth, - shouldStroke = false; - - ctx.beginPath(); - this.forEachControl(function (control, key, fabricObject) { - // in this moment, the ctx is centered on the object. - // width and height of the above function are the size of the bbox. - if (control.withConnection && control.getVisibility(fabricObject, key)) { - // reset movement for each control - shouldStroke = true; - ctx.moveTo(control.x * width, control.y * height); - ctx.lineTo( - control.x * width + control.offsetX, - control.y * height + control.offsetY - ); - } - }); - shouldStroke && ctx.stroke(); - - return this; - }, - /** * Draws corners of an object's bounding box. * Requires public properties: width, height @@ -17688,7 +17939,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot drawControls: function(ctx, styleOverride) { styleOverride = styleOverride || {}; ctx.save(); - var retinaScaling = this.canvas.getRetinaScaling(), p; + var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; if (!this.transparentCorners) { @@ -17696,9 +17947,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); this.setCoords(); + if (this.group) { + // fabricJS does not really support drawing controls inside groups, + // this piece of code here helps having at least the control in places. + // If an application needs to show some objects as selected because of some UI state + // can still call Object._renderControls() on any object they desire, independently of groups. + // using no padding, circular controls and hiding the rotating cursor is higly suggested, + matrix = this.group.calcTransformMatrix(); + } this.forEachControl(function(control, key, fabricObject) { + p = fabricObject.oCoords[key]; if (control.getVisibility(fabricObject, key)) { - p = fabricObject.oCoords[key]; + if (matrix) { + p = fabric.util.transformPoint(p, matrix); + } control.render(ctx, p.x, p.y, styleOverride, fabricObject); } }); @@ -17808,7 +18070,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, startValue: object.left, - endValue: this.getCenterPoint().x, + endValue: this.getCenter().left, duration: this.FX_DURATION, onChange: function(value) { object.set('left', value); @@ -17841,7 +18103,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, startValue: object.top, - endValue: this.getCenterPoint().y, + endValue: this.getCenter().top, duration: this.FX_DURATION, onChange: function(value) { object.set('top', value); @@ -18299,15 +18561,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Line * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument */ - fabric.Line.fromObject = function(object) { + fabric.Line.fromObject = function(object, callback) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; var options = clone(object, true); options.points = [object.x1, object.y1, object.x2, object.y2]; - return fabric.Object._fromObject(fabric.Line, options, 'points').then(function(fabricLine) { - delete fabricLine.points; - return fabricLine; - }); + fabric.Object._fromObject('Line', options, _callback, 'points'); }; /** @@ -18540,10 +18803,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Circle * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument + * @return {void} */ - fabric.Circle.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Circle, object); + fabric.Circle.fromObject = function(object, callback) { + fabric.Object._fromObject('Circle', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -18635,10 +18899,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Triangle * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument */ - fabric.Triangle.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Triangle, object); + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -18817,10 +19081,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument + * @return {void} */ - fabric.Ellipse.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Ellipse, object); + fabric.Ellipse.fromObject = function(object, callback) { + fabric.Object._fromObject('Ellipse', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19006,10 +19271,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Rect * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created */ - fabric.Rect.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Rect, object); + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19276,10 +19541,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polyline * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created */ - fabric.Polyline.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Polyline, object, 'points'); + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19358,10 +19623,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polygon * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @return {void} */ - fabric.Polygon.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Polygon, object, 'points'); + fabric.Polygon.fromObject = function(object, callback) { + fabric.Object._fromObject('Polygon', object, callback, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19376,6 +19642,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot max = fabric.util.array.max, extend = fabric.util.object.extend, clone = fabric.util.object.clone, + _toString = Object.prototype.toString, toFixed = fabric.util.toFixed; if (fabric.Path) { @@ -19429,8 +19696,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Object} [options] Options object */ _setPath: function (path, options) { + var fromArray = _toString.call(path) === '[object Array]'; + this.path = fabric.util.makePathSimpler( - Array.isArray(path) ? path : fabric.util.parsePath(path) + fromArray ? path : fabric.util.parsePath(path) ); fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); @@ -19701,10 +19970,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Path * @param {Object} object - * @returns {Promise} - */ - fabric.Path.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Path, object, 'path'); + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.sourcePath === 'string') { + var pathUrl = object.sourcePath; + fabric.loadSVGFromURL(pathUrl, function (elements) { + var path = elements[0]; + path.setOptions(object); + callback && callback(path); + }); + } + else { + fabric.Object._fromObject('Path', object, callback, 'path'); + } }; /* _FROM_SVG_START_ */ @@ -19854,7 +20133,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change */ _updateObjectsCoords: function(center) { - var center = center || this.getRelativeCenterPoint(); + var center = center || this.getCenterPoint(); for (var i = this._objects.length; i--; ){ this._updateObjectCoords(this._objects[i], center); } @@ -20296,15 +20575,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Group * @param {Object} object Object to create a group from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an group instance is created */ - fabric.Group.fromObject = function(object) { - var objects = object.objects || [], + fabric.Group.fromObject = function(object, callback) { + var objects = object.objects, options = fabric.util.object.clone(object, true); delete options.objects; - return fabric.util.enlivenObjects(objects).then(function (enlivenedObjects) { - return fabric.util.enlivenObjectEnlivables(options).then(function(enlivedProps) { - return new fabric.Group(enlivenedObjects, Object.assign(options, enlivedProps), true); + if (typeof objects === 'string') { + // it has to be an url or something went wrong. + fabric.loadSVGFromURL(objects, function (elements) { + var group = fabric.util.groupSVGElements(elements, object, objects); + group.set(options); + callback && callback(group); + }); + return; + } + fabric.util.enlivenObjects(objects, function (enlivenedObjects) { + var options = fabric.util.object.clone(object, true); + delete options.objects; + fabric.util.enlivenObjectEnlivables(object, options, function () { + callback && callback(new fabric.Group(enlivenedObjects, options, true)); }); }); }; @@ -20457,14 +20747,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.ActiveSelection * @param {Object} object Object to create a group from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created */ - fabric.ActiveSelection.fromObject = function(object) { - var objects = object.objects, - options = fabric.util.object.clone(object, true); - delete options.objects; - return fabric.util.enlivenObjects(objects).then(function(enlivenedObjects) { - return new fabric.ActiveSelection(enlivenedObjects, object, true); + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); }); }; @@ -20615,6 +20903,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Please check video element events for seeking. * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. * @return {fabric.Image} thisArg */ initialize: function(element, options) { @@ -20842,18 +21131,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Sets source of an image * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) * @param {Object} [options] Options object * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @return {Promise} thisArg + * @return {fabric.Image} thisArg + * @chainable */ - setSrc: function(src, options) { - var _this = this; - return fabric.util.loadImage(src, options).then(function(img) { - _this.setElement(img, options); - _this._setWidthHeight(); - return _this; - }); + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img, isError) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this, isError); + }, this, options && options.crossOrigin); + return this; }, /** @@ -20868,8 +21159,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var filter = this.resizeFilter, minimumScale = this.minimumScaleTrigger, objectScale = this.getTotalObjectScaling(), - scaleX = objectScale.x, - scaleY = objectScale.y, + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, elementToFilter = this._filteredEl || this._originalElement; if (this.group) { this.set('dirty', true); @@ -21025,7 +21316,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _needsResize: function() { var scale = this.getTotalObjectScaling(); - return (scale.x !== this._lastScaleX || scale.y !== this._lastScaleY); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); }, /** @@ -21057,6 +21348,22 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._setWidthHeight(options); }, + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + /** * @private * Set the width and the height of the image object, using the element or the @@ -21154,39 +21461,39 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} - */ - fabric.Image.fromObject = function(_object) { - var object = fabric.util.object.clone(_object), - filters = object.filters, - resizeFilter = object.resizeFilter; - // the generic enliving will fail on filters for now - delete object.resizeFilter; - delete object.filters; - return Promise.all([ - fabric.util.loadImage(object.src, { crossOrigin: _object.crossOrigin }), - filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'), - resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'), - fabric.util.enlivenObjectEnlivables(object), - ]) - .then(function(imgAndFilters) { - object.filters = imgAndFilters[1] || []; - object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; - return new fabric.Image(imgAndFilters[0], Object.assign(object, imgAndFilters[3])); + * @param {Function} callback Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, isError) { + if (isError) { + callback && callback(null, true); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjectEnlivables(object, object, function () { + var image = new fabric.Image(img, object); + callback(image, false); + }); + }); }); + }, null, object.crossOrigin); }; /** * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. * @param {Object} [imgOptions] Options object - * @returns {Promise} */ - fabric.Image.fromURL = function(url, imgOptions) { - return fabric.util.loadImage(url, imgOptions || {}).then(function(img) { - return new fabric.Image(img, imgOptions); - }); + fabric.Image.fromURL = function(url, callback, imgOptions) { + fabric.util.loadImage(url, function(img, isError) { + callback && callback(new fabric.Image(img, imgOptions), isError); + }, null, imgOptions && imgOptions.crossOrigin); }; /* _FROM_SVG_START_ */ @@ -21210,10 +21517,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ fabric.Image.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - fabric.Image.fromURL(parsedAttributes['xlink:href'], Object.assign({ }, options || { }, parsedAttributes)) - .then(function(fabricImage) { - callback(fabricImage); - }); + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); }; /* _FROM_SVG_END_ */ @@ -22123,14 +22428,10 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag } }); -/** - * Create filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @returns {Promise} - */ -fabric.Image.filters.BaseFilter.fromObject = function(object) { - return Promise.resolve(new fabric.Image.filters[object.type](object)); +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; }; @@ -22285,10 +22586,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix */ fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); @@ -22398,10 +22700,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness */ fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -22751,10 +23054,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute */ fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -22906,10 +23210,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale */ fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23017,10 +23322,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert */ fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23153,10 +23459,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise */ fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23291,10 +23598,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate */ fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23465,10 +23773,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite */ fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23804,10 +24113,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor */ fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24046,16 +24356,17 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} callback to be invoked after filter creation + * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage */ - fabric.Image.filters.BlendImage.fromObject = function(object) { - return fabric.Image.fromObject(object.image).then(function(image) { + fabric.Image.filters.BlendImage.fromObject = function(object, callback) { + fabric.Image.fromObject(object.image, function(image) { var options = fabric.util.object.clone(object); options.image = image; - return new fabric.Image.filters.BlendImage(options); + callback(new fabric.Image.filters.BlendImage(options)); }); }; @@ -24543,10 +24854,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize */ fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24657,10 +24969,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast */ fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24716,7 +25029,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * Saturation value, from -1 to 1. * Increases/decreases the color saturation. * A value of 0 has no effect. - * + * * @param {Number} saturation * @default */ @@ -24777,10 +25090,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate */ fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24837,7 +25151,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * Vibrance value, from -1 to 1. * Increases/decreases the saturation of more muted colors with less effect on saturated colors. * A value of 0 has no effect. - * + * * @param {Number} vibrance * @default */ @@ -24900,10 +25214,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance */ fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25122,10 +25437,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @returns {Promise} + * Deserialize a JSON definition of a BlurFilter into a concrete instance. */ filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25259,10 +25571,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma */ fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25331,13 +25644,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { /** * Deserialize a JSON definition of a ComposedFilter into a concrete instance. */ - fabric.Image.filters.Composed.fromObject = function(object) { - var filters = object.subFilters || []; - return Promise.all(filters.map(function(filter) { - return fabric.Image.filters[filter.type].fromObject(filter); - })).then(function(enlivedFilters) { - return new fabric.Image.filters.Composed({ subFilters: enlivedFilters }); - }); + fabric.Image.filters.Composed.fromObject = function(object, callback) { + var filters = object.subFilters || [], + subFilters = filters.map(function(filter) { + return new fabric.Image.filters[filter.type](filter); + }), + instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); + callback && callback(instance); + return instance; }; })(typeof exports !== 'undefined' ? exports : this); @@ -25440,10 +25754,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation */ fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25574,12 +25889,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { linethrough: false, /** - * Text alignment. - * @typedef {'start' | 'center' | 'end' | 'left' | 'right' | 'justify' | 'justify-start' | 'justify-center' | 'justify-end' | 'justify-left' | 'justify-right'} TextAlign - * @type {TextAlign} + * Text alignment. Possible values: "left", "center", "right", "justify", + * "justify-left", "justify-center" or "justify-right". + * @type String * @default */ - textAlign: 'start', + textAlign: 'left', /** * Font style . Possible values: "", "normal", "italic" or "oblique". @@ -25687,10 +26002,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { /** * Which side of the path the text should be drawn on. * Only used when text has a path - * @type {'left'|'right'|'start'|'end'} + * @type {String} 'left|right' * @default */ - pathSide: 'start', + pathSide: 'left', /** * How text is aligned to the path. This property determines @@ -25830,35 +26145,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { 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 @@ -26286,8 +26572,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { 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._isPathReversed(), - textAlign = this.textAlign; + reverse = this.pathSide === 'right'; this.__charBounds[lineIndex] = lineBounds; for (i = 0; i < line.length; i++) { @@ -26310,31 +26595,17 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); startingPoint.x += path.pathOffset.x; startingPoint.y += path.pathOffset.y; - var size = totalPathLength - width; - switch (textAlign) { - case 'start': - case 'justify': - case 'justify-start': - positionInPath = 0; - break; - case 'end': - case 'justify-end': - positionInPath = size; - break; + switch (this.textAlign) { case 'left': - case 'justify-left': - positionInPath = reverse ? size : 0; + positionInPath = reverse ? (totalPathLength - width) : 0; break; case 'center': - case 'justify-center': - positionInPath = size / 2; + positionInPath = (totalPathLength - width) / 2; break; case 'right': - case 'justify-right': - positionInPath = reverse ? 0 : size; - break; - default: + positionInPath = reverse ? 0 : (totalPathLength - width); break; + //todo - add support for justify } positionInPath += this.pathStartOffset * (reverse ? -1 : 1); for (i = reverse ? line.length - 1 : 0; @@ -26372,7 +26643,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { 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._isPathReversed() ? Math.PI : 0); + graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); }, /** @@ -26540,9 +26811,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - // this was changed in the PR #7674 - // currentDirection = ctx.canvas.getAttribute('dir'); - drawingLeft, currentDirection = ctx.direction; + drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); @@ -26782,35 +27051,29 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { */ _getLineLeftOffset: function(lineIndex) { var lineWidth = this.getLineWidth(lineIndex), - lineDiff = this.width - lineWidth, textAlign = this.textAlign, rtl = this.direction === 'rtl', + lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); - if (textAlign === 'justify' || (textAlign.startsWith('justify') && !isEndOfWrapping)) { + if (textAlign === 'justify' + || (textAlign === 'justify-center' && !isEndOfWrapping) + || (textAlign === 'justify-right' && !isEndOfWrapping) + || (textAlign === 'justify-left' && !isEndOfWrapping) + ) { return 0; } - switch (textAlign) { - case 'start': - case 'justify': - case 'justify-start': - leftOffset = 0; - 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; + 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') { + leftOffset -= lineDiff; } return leftOffset; }, @@ -27203,10 +27466,22 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * @static * @memberOf fabric.Text * @param {Object} object plain js Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created */ - fabric.Text.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Text, object, 'text'); + fabric.Text.fromObject = function(object, callback) { + var objectCopy = clone(object), path = object.path; + delete objectCopy.path; + return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { + if (path) { + fabric.Object._fromObject('Path', path, function(pathInstance) { + textInstance.set('path', pathInstance); + callback(textInstance); + }, 'path'); + } + else { + callback(textInstance); + } + }, 'text'); }; fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; @@ -27543,6 +27818,16 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { (function() { + + function parseDecoration(object) { + if (object.textDecoration) { + object.textDecoration.indexOf('underline') > -1 && (object.underline = true); + object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); + object.textDecoration.indexOf('overline') > -1 && (object.overline = true); + delete object.textDecoration; + } + } + /** * IText class (introduced in v1.4) Events are also fired with "text:" * prefix when observing canvas. @@ -27612,15 +27897,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { */ 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 @@ -27757,45 +28033,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { 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' @@ -27948,31 +28185,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - 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; - } + boundaries.left *= -1; } this.cursorOffsetCache = boundaries; return this.cursorOffsetCache; @@ -28061,31 +28274,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - 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; - } + drawStart = this.width - drawStart - drawWidth; } ctx.fillRect( drawStart, @@ -28137,10 +28326,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * @static * @memberOf fabric.IText * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as argument */ - fabric.IText.fromObject = function(object) { - return fabric.Object._fromObject(fabric.IText, object, 'text'); + fabric.IText.fromObject = function(object, callback) { + parseDecoration(object); + if (object.styles) { + for (var i in object.styles) { + for (var j in object.styles[i]) { + parseDecoration(object.styles[i][j]); + } + } + } + fabric.Object._fromObject('IText', object, callback, 'text'); }; })(); @@ -28335,24 +28532,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findWordBoundary: function (from, startFrom) { - return from === 'right' ? - this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findWordBoundaryStart: function(startFrom) { + findWordBoundaryLeft: function(startFrom) { var offset = 0, index = startFrom - 1; // remove space before cursor first @@ -28367,7 +28552,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { index--; } - return Math.max(startFrom - offset, 0); + return startFrom - offset; }, /** @@ -28375,7 +28560,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findWordBoundaryEnd: function(startFrom) { + findWordBoundaryRight: function(startFrom) { var offset = 0, index = startFrom; // remove space after cursor first @@ -28393,24 +28578,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { return startFrom + offset; }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findLineBoundary: function (from, startFrom) { - return from === 'right' ? - this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findLineBoundaryStart: function(startFrom) { + findLineBoundaryLeft: function(startFrom) { var offset = 0, index = startFrom - 1; while (!/\n/.test(this._text[index]) && index > -1) { @@ -28418,7 +28591,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { index--; } - return Math.max(startFrom - offset, 0); + return startFrom - offset; }, /** @@ -28426,7 +28599,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * @param {Number} startFrom Current selection index * @return {Number} New selection index */ - findLineBoundaryEnd: function(startFrom) { + findLineBoundaryRight: function(startFrom) { var offset = 0, index = startFrom; while (!/\n/.test(this._text[index]) && index < this._text.length) { @@ -28484,8 +28657,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { */ selectLine: function(selectionStart) { selectionStart = selectionStart || this.selectionStart; - var newSelectionStart = this.findLineBoundaryStart(selectionStart), - newSelectionEnd = this.findLineBoundaryEnd(selectionStart); + var newSelectionStart = this.findLineBoundaryLeft(selectionStart), + newSelectionEnd = this.findLineBoundaryRight(selectionStart); this.selectionStart = newSelectionStart; this.selectionEnd = newSelectionEnd; @@ -28561,19 +28734,19 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { currentStart = this.selectionStart, currentEnd = this.selectionEnd; if ( - (newSelectionStart !== this.__selectionStartOrigin || currentStart === currentEnd) + (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) && (currentStart === newSelectionStart || currentEnd === newSelectionStart) ) { return; } - if (newSelectionStart > this.__selectionStartOrigin) { - this.selectionStart = this.__selectionStartOrigin; + if (newSelectionStart > this.__selectionStartOnMouseDown) { + this.selectionStart = this.__selectionStartOnMouseDown; this.selectionEnd = newSelectionStart; } else { this.selectionStart = newSelectionStart; - this.selectionEnd = this.__selectionStartOrigin; + this.selectionEnd = this.__selectionStartOnMouseDown; } if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { this.restartCursorIfNeeded(); @@ -28619,15 +28792,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { var smallerTextStart = _text.slice(0, start), graphemeStart = smallerTextStart.join('').length; if (start === end) { - return { selectionStart: graphemeStart, selectionEnd: graphemeStart, selectionDirection: 'forward' }; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; } var smallerTextEnd = _text.slice(start, end), graphemeEnd = smallerTextEnd.join('').length; - return { - selectionStart: graphemeStart, - selectionEnd: graphemeStart + graphemeEnd, - selectionDirection: graphemeStart < this.__selectionStartOrigin ? 'backward' : 'forward' - }; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; }, /** @@ -28640,12 +28809,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { } if (!this.inCompositionMode) { var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text); - this.hiddenTextarea.setSelectionRange( - newSelection.selectionStart, - newSelection.selectionEnd, - newSelection.selectionDirection - ); - this.selectionDirection = newSelection.selectionDirection; + this.hiddenTextarea.selectionStart = newSelection.selectionStart; + this.hiddenTextarea.selectionEnd = newSelection.selectionEnd; } this.updateTextareaPosition(); }, @@ -28669,7 +28834,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { if (!this.inCompositionMode) { this.selectionStart = newSelection.selectionStart; } - this.selectionDirection = newSelection.selectionDirection; this.updateTextareaPosition(); }, @@ -29071,14 +29235,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { if (end === start) { this._selectionDirection = 'left'; } - else if (this.selectionDirection === 'forward') { + else if (this._selectionDirection === 'right') { this._selectionDirection = 'left'; this.selectionEnd = start; } this.selectionStart = newSelection; } else if (newSelection > start && newSelection < end) { - if (this.selectionDirection === 'forward') { + if (this._selectionDirection === 'right') { this.selectionEnd = newSelection; } else { @@ -29090,7 +29254,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { if (end === start) { this._selectionDirection = 'right'; } - else if (this.selectionDirection === 'backward') { + else if (this._selectionDirection === 'left') { this._selectionDirection = 'right'; this.selectionStart = end; } @@ -29228,7 +29392,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } if (this.isEditing) { - this.__selectionStartOrigin = this.selectionStart; + this.__selectionStartOnMouseDown = this.selectionStart; if (this.selectionStart === this.selectionEnd) { this.abortCursorAnimation(); } @@ -29349,7 +29513,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); + lineLeftOffset = this._getLineLeftOffset(lineIndex); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -29357,7 +29521,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; @@ -29456,11 +29620,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorForward', - 36: 'moveCursorBackward', - 37: 'moveCursorBackward', + 35: 'moveCursorRight', + 36: 'moveCursorLeft', + 37: 'moveCursorLeft', 38: 'moveCursorUp', - 39: 'moveCursorForward', + 39: 'moveCursorRight', 40: 'moveCursorDown', }, @@ -29469,11 +29633,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 27: 'exitEditing', 33: 'moveCursorUp', 34: 'moveCursorDown', - 35: 'moveCursorBackward', - 36: 'moveCursorForward', - 37: 'moveCursorForward', + 35: 'moveCursorLeft', + 36: 'moveCursorRight', + 37: 'moveCursorRight', 38: 'moveCursorUp', - 39: 'moveCursorBackward', + 39: 'moveCursorLeft', 40: 'moveCursorDown', }, @@ -29869,7 +30033,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @param {Number} offset */ moveCursorWithShift: function(offset) { - var newSelection = this.selectionDirection === 'backward' + var newSelection = this._selectionDirection === 'left' ? this.selectionStart + offset : this.selectionEnd + offset; this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); @@ -29893,94 +30057,111 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Moves cursor back + * Moves cursor left * @param {Event} e Event object */ - moveCursorBackward: function (e) { + moveCursorLeft: function(e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } - 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); - } + 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]); } else { - changed = true; -<<<<<<< HEAD - //this._selectionDirection = 'left'; -======= + 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'; ->>>>>>> 89582310 (fix(tests)) - // 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; - } + return this._moveLeft(e, 'selectionStart'); } - this._invalidateCursor(changed); }, /** - * Moves cursor forward + * Moves cursor right * @param {Event} e Event object */ - moveCursorForward: function (e) { + moveCursorRight: function(e) { if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } - 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; -<<<<<<< HEAD - //this._selectionDirection = 'right'; -======= - this._selectionDirection = 'right'; ->>>>>>> 89582310 (fix(tests)) - 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); + this._moveCursorLeftOrRight('Right', e); }, /** - * @private - * @param {boolean} dirty + * Moves cursor right or Left, fires event + * @param {String} direction 'Left', 'Right' + * @param {Event} e Event object */ - _invalidateCursor: function (dirty) { + _moveCursorLeftOrRight: function(direction, e) { + var actionName = 'moveCursor' + direction + 'With'; this._currentCursorOpacity = 1; - if (dirty) { + + if (e.shiftKey) { + actionName += 'Shift'; + } + else { + actionName += 'outShift'; + } + if (this[actionName](e)) { this.abortCursorAnimation(); this.initDelayedCursor(); this._fireSelectionChanged(); @@ -29989,36 +30170,35 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * @private - * @param {Event} e - * @param {'selectionStart'|'selectionEnd'} prop - * @param {number} direction - * @returns {boolean} true if a change happened + * Moves cursor right while keeping selection + * @param {Event} e */ - _move: function(e, prop, direction) { - var newValue; - direction = Math.sign(direction); - if (direction === 0) { - return false; + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + return this._moveRight(e, 'selectionStart'); } - if (e.altKey) { - newValue = direction > 0 ? - this.findWordBoundaryEnd(this[prop]) : - this.findWordBoundaryStart(this[prop]); + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + return this._moveRight(e, 'selectionEnd'); } - else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { - newValue = direction > 0 ? - this.findLineBoundaryEnd(this[prop]) : - this.findLineBoundaryStart(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 { - 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; + this.selectionStart = this.selectionEnd; } + return changed; }, /** @@ -30132,7 +30312,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), - (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -30155,9 +30334,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); - if (this.direction === 'rtl') { - lineOffset += this.width; - } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } @@ -30232,12 +30408,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - if (this.direction === 'rtl') { - textLeftOffset -= boxWidth; - } - else { - textLeftOffset += boxWidth; - } + textLeftOffset += boxWidth; boxWidth = 0; } } @@ -30781,10 +30952,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @static * @memberOf fabric.Textbox * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created */ - fabric.Textbox.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Textbox, object, 'text'); + fabric.Textbox.fromObject = function(object, callback) { + return fabric.Object._fromObject('Textbox', object, callback, 'text'); }; })(typeof exports !== 'undefined' ? exports : this); From 71304b76565a77d766bc4e3214e6d525f8b1fdbe Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 07:30:22 +0300 Subject: [PATCH 49/61] Update itext_behavior.mixin.js --- src/mixins/itext_behavior.mixin.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index f04bd218b1e..fe75d7577d3 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -188,18 +188,6 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findWordBoundary: function (from, startFrom) { - return from === 'right' ? - this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index From 17d539e0629bcce2caf6e8fabb270ff4a204c9d2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 07:39:10 +0300 Subject: [PATCH 50/61] Update object_origin.mixin.js --- src/mixins/object_origin.mixin.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/mixins/object_origin.mixin.js b/src/mixins/object_origin.mixin.js index 34c205a9883..183849d878b 100644 --- a/src/mixins/object_origin.mixin.js +++ b/src/mixins/object_origin.mixin.js @@ -22,10 +22,18 @@ /** * Resolves origin value relative to center * @private - * @param {OriginX} originX + * @param {OriginX|'start'|'end'} originX * @returns number */ resolveOriginX: function (originX) { + switch (originX) { + case 'start': + originX = this.direction === 'rtl' ? 'right' : 'left'; + break; + case 'end': + originX = this.direction === 'rtl' ? 'left' : 'right'; + break; + } return typeof originX === 'string' ? originXOffset[originX] : originX - 0.5; From da510f747504a2109e227b60e32470992df02089 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 07:40:46 +0300 Subject: [PATCH 51/61] Update text.class.js --- src/shapes/text.class.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 79b2abece9b..2d2470c0738 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -128,6 +128,11 @@ * @default */ textAlign: 'start', + + /** + * @override + */ + originX: 'start', /** * Font style . Possible values: "", "normal", "italic" or "oblique". @@ -307,7 +312,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 From c387058bc9b55396a54400750aab48e2675b37fd Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 07:45:11 +0300 Subject: [PATCH 52/61] test --- test/unit/text.js | 2 +- test/unit/textbox.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/text.js b/test/unit/text.js index 3d9615f8cba..fe810073be6 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -11,7 +11,7 @@ var REFERENCE_TEXT_OBJECT = { version: fabric.version, type: 'text', - originX: 'left', + originX: 'start', originY: 'top', left: 0, top: 0, diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 1f58625af47..2ce8c1ba094 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -9,7 +9,7 @@ var TEXTBOX_OBJECT = { version: fabric.version, type: 'textbox', - originX: 'left', + originX: 'start', originY: 'top', left: 0, top: 0, From 30b8d68c8db57c739beb29900fd7664ea010ca0e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 07:48:29 +0300 Subject: [PATCH 53/61] Update text.js --- test/unit/text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/text.js b/test/unit/text.js index fe810073be6..6d7d22886ac 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -207,7 +207,7 @@ width: 8, height: 18.08, fontSize: 16, - originX: 'left' + originX: 'start' }); assert.deepEqual(text.toObject(), expectedObject, 'parsed object is what expected'); }); From dda2b8a8cad8851dfafa40e5f34867b4cd470104 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 08:05:57 +0300 Subject: [PATCH 54/61] Update itext_key_behaviour.js --- test/unit/itext_key_behaviour.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 5c9bfbabb67..5833fcdaf8b 100644 --- a/test/unit/itext_key_behaviour.js +++ b/test/unit/itext_key_behaviour.js @@ -110,21 +110,27 @@ assert.equal(iText.selectionEnd, 9, 'should move to upper line end'); selection = 0; - iText.selectionStart = 1; - iText.selectionEnd = 4; + iText.setSelectionRange(1, 4, 'forward'); iText.moveCursorDown({ shiftKey: false }); assert.equal(selection, 1, '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.setSelectionRange(28, 31, 'backward'); iText.moveCursorBackward({ shiftKey: false }); assert.equal(selection, 1, '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'); + iText.moveCursorBackward({ shiftKey: false }); + assert.equal(selection, 1, '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 From 7b1f608083f9216cff70256b9e693455aefd267a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 08:20:23 +0300 Subject: [PATCH 55/61] Update textbox.js --- test/unit/textbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 2ce8c1ba094..f314e4dd1b5 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -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); From 5c5eb9911a8025375072f61827ce76d31b69a2d0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 30 Mar 2022 21:48:01 +0300 Subject: [PATCH 56/61] revert --- src/mixins/object_origin.mixin.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/mixins/object_origin.mixin.js b/src/mixins/object_origin.mixin.js index 183849d878b..34c205a9883 100644 --- a/src/mixins/object_origin.mixin.js +++ b/src/mixins/object_origin.mixin.js @@ -22,18 +22,10 @@ /** * Resolves origin value relative to center * @private - * @param {OriginX|'start'|'end'} originX + * @param {OriginX} originX * @returns number */ resolveOriginX: function (originX) { - switch (originX) { - case 'start': - originX = this.direction === 'rtl' ? 'right' : 'left'; - break; - case 'end': - originX = this.direction === 'rtl' ? 'left' : 'right'; - break; - } return typeof originX === 'string' ? originXOffset[originX] : originX - 0.5; From 1f34b3a6fdbb3f60b689677303cda610ac971cc1 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 08:34:20 +0300 Subject: [PATCH 57/61] cleanup --- src/mixins/itext_behavior.mixin.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 91cc1d12f62..f6bde5a4692 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -193,18 +193,6 @@ return this._text.slice(this.selectionStart, this.selectionEnd).join(''); }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findWordBoundary: function (from, startFrom) { - return from === 'right' ? - this.findWordBoundaryRight(startFrom) : - this.findWordBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Current selection index From 1f031326db15e1b9c432598b5df9d725398c646e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 08:36:35 +0300 Subject: [PATCH 58/61] cleanup --- src/mixins/itext_behavior.mixin.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index f6bde5a4692..73f5ccff35b 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -239,18 +239,6 @@ return startFrom + offset; }, - /** - * - * @param {'left'|'right'} from - * @param {number} startFrom - * @return {number} New selection index - */ - findLineBoundary: function (from, startFrom) { - return from === 'right' ? - this.findLineBoundaryRight(startFrom) : - this.findLineBoundaryLeft(startFrom); - }, - /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index From 1e5dad79f6b859310d0baeba5b21fb10f5c4ede1 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 08:59:32 +0300 Subject: [PATCH 59/61] Create out.txt --- test/unit/textbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/textbox.js b/test/unit/textbox.js index f314e4dd1b5..05af638cffa 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -9,7 +9,7 @@ var TEXTBOX_OBJECT = { version: fabric.version, type: 'textbox', - originX: 'start', + originX: 'left', originY: 'top', left: 0, top: 0, From 86add7c815c5f3a144989f6d33e125419e558a9f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 10:37:22 +0300 Subject: [PATCH 60/61] fix(): tests --- test/unit/itext_key_behaviour.js | 28 ++++++++++++++-------------- test/unit/text.js | 3 +-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 5833fcdaf8b..4a6ca9db599 100644 --- a/test/unit/itext_key_behaviour.js +++ b/test/unit/itext_key_behaviour.js @@ -111,22 +111,25 @@ selection = 0; 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.setSelectionRange(28, 31, 'backward'); + assert.equal(selection, 1, 'should fire after setting selection range'); iText.moveCursorBackward({ shiftKey: false }); - assert.equal(selection, 1, 'should fire'); + 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, 1, 'should fire'); + 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; @@ -218,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.setSelectionRange(0, 1, 'backward'); + assert.equal(selection, 1, 'should fire after setting selection range'); iText.moveCursorBackward({ 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'); 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.setSelectionRange(30, 31, 'forward'); + assert.equal(selection, 1, 'should fire after setting selection range'); iText.moveCursorForward({ 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'); 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 6d7d22886ac..d7ad6e34e75 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -11,7 +11,7 @@ var REFERENCE_TEXT_OBJECT = { version: fabric.version, type: 'text', - originX: 'start', + originX: 'left', originY: 'top', left: 0, top: 0, @@ -207,7 +207,6 @@ width: 8, height: 18.08, fontSize: 16, - originX: 'start' }); assert.deepEqual(text.toObject(), expectedObject, 'parsed object is what expected'); }); From 8b1451a93123f8d4f59ffba9102dfc99c2ecd21b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 10:37:26 +0300 Subject: [PATCH 61/61] Update itext_key_behavior.mixin.js --- src/mixins/itext_key_behavior.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 856b2a437df..53a8d77c7f0 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -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); }