From f750f9d0333c18fe4a53ada5742e73213b35579c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 20 Dec 2022 22:19:25 +0200 Subject: [PATCH 01/52] Update itext_behavior.mixin.ts --- src/mixins/itext_behavior.mixin.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 4e6c2600080..96cab423717 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -7,6 +7,7 @@ import { Text } from '../shapes/text.class'; import { TPointerEvent } from '../typedefs'; import { setStyle } from '../util/dom_style'; import { removeFromArray } from '../util/internals'; +import { clone } from '../util/lang_object'; import { createCanvasElement } from '../util/misc/dom'; import { transformPoint } from '../util/misc/matrix'; import { Canvas } from '../__types__'; @@ -564,7 +565,7 @@ export abstract class ITextBehaviorMixin< const offset = correction.add(diff).scalarMultiply(retinaScaling); // prepare instance for drag image snapshot by making all non selected text invisible const bgc = this.backgroundColor; - const styles = object.clone(this.styles, true); + const styles = clone(this.styles, true); delete this.backgroundColor; const styleOverride = { fill: 'transparent', @@ -679,7 +680,8 @@ export abstract class ITextBehaviorMixin< * @param {object} options * @param {DragEvent} options.e */ - dragOverHandler({ e }: TEvent) { + dragOverHandler(ev: TEvent) { + const { e } = ev; const canDrop = !e.defaultPrevented && this.canDrop(e); if (!this.__isDraggingOver && canDrop) { this.__isDraggingOver = true; @@ -691,8 +693,8 @@ export abstract class ITextBehaviorMixin< // can be dropped, inform browser e.preventDefault(); // inform event subscribers - options.canDrop = true; - options.dropTarget = this; + ev.canDrop = true; + ev.dropTarget = this; // find cursor under the drag part. } } From 617bc87667ac8c33554c1a2830dbb610f8615c1f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 10:42:03 +0200 Subject: [PATCH 02/52] Update itext_behavior.mixin.ts --- src/mixins/itext_behavior.mixin.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 96cab423717..3da42afe2a3 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -618,7 +618,7 @@ export abstract class ITextBehaviorMixin< const value = this._text .slice(selection.selectionStart, selection.selectionEnd) .join(''); - const data = Object.assign({ text: this.text, value: value }, selection); + const data = { text: this.text, value, ...selection }; e.dataTransfer.setData('text/plain', value); e.dataTransfer.setData( 'application/fabric', @@ -764,10 +764,9 @@ export abstract class ITextBehaviorMixin< * in order to change the drop value or to customize styling respectively, by listening to the `drop:before` event * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#performing_a_drop * @private - * @param {object} options - * @param {DragEvent} options.e */ - dropHandler({ e }: TEvent) { + dropHandler(ev: TEvent) { + const { e } = ev; const didDrop = e.defaultPrevented; this.__isDraggingOver = false; // inform browser that the drop has been accepted @@ -803,8 +802,8 @@ export abstract class ITextBehaviorMixin< insert = insert.trimEnd(); } // inform subscribers - options.didDrop = true; - options.dropTarget = this; + ev.didDrop = true; + ev.dropTarget = this; // finalize this.insertChars(insert, styles, insertAt); // can this part be moved in an outside event? andrea to check. From db8739a8f72ea77645424bbf6ab678411477be2f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 11:28:15 +0200 Subject: [PATCH 03/52] fix(): bad conflict resolution d69e3dd58b4a3b45b61dfd4006fb1a1b7452936e of #8013 --- src/mixins/itext_click_behavior.mixin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mixins/itext_click_behavior.mixin.ts b/src/mixins/itext_click_behavior.mixin.ts index 6cc5188304f..4530ced2fd3 100644 --- a/src/mixins/itext_click_behavior.mixin.ts +++ b/src/mixins/itext_click_behavior.mixin.ts @@ -106,6 +106,7 @@ export abstract class ITextClickBehaviorMixin< if ( !this.canvas || !this.editable || + this.__isDragging || (options.e.button && options.e.button !== 1) ) { return; From 5555c5c9d2a6c4209dabf829ebc36ad46dd58f7d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 11:44:16 +0200 Subject: [PATCH 04/52] fix no dpr on drag image offset --- src/mixins/itext_behavior.mixin.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 3da42afe2a3..61d66f49984 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -548,21 +548,20 @@ export abstract class ITextBehaviorMixin< value: string; } ) { - const t = this.calcTransformMatrix(); const flipFactor = new Point(this.flipX ? -1 : 1, this.flipY ? -1 : 1); const boundaries = this._getCursorBoundaries(data.selectionStart); const selectionPosition = new Point( boundaries.left + boundaries.leftOffset, boundaries.top + boundaries.topOffset ).multiply(flipFactor); - const pos = transformPoint(selectionPosition, t); + const pos = selectionPosition.transform(this.calcTransformMatrix()); const pointer = this.canvas.getPointer(e); const diff = pointer.subtract(pos); const enableRetinaScaling = this.canvas._isRetinaScaling(); const retinaScaling = this.getCanvasRetinaScaling(); const bbox = this.getBoundingRect(true); const correction = pos.subtract(new Point(bbox.left, bbox.top)); - const offset = correction.add(diff).scalarMultiply(retinaScaling); + const offset = correction.add(diff); // prepare instance for drag image snapshot by making all non selected text invisible const bgc = this.backgroundColor; const styles = clone(this.styles, true); @@ -774,10 +773,9 @@ export abstract class ITextBehaviorMixin< let insert = e.dataTransfer.getData('text/plain'); if (insert && !didDrop) { let insertAt = this.getSelectionStartFromPointer(e); - const data = e.dataTransfer.types.includes('application/fabric') + const { styles } = e.dataTransfer.types.includes('application/fabric') ? JSON.parse(e.dataTransfer.getData('application/fabric')) : {}; - const styles = data.styles; const trailing = insert[Math.max(0, insert.length - 1)]; const selectionStartOffset = 0; // drag and drop in same instance From df950651b6c091167cd3629b9acd7af01fb11253 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 12:16:56 +0200 Subject: [PATCH 05/52] fix(): drag image + vpt --- src/mixins/itext_behavior.mixin.ts | 29 ++++++++++++++++++----------- src/util/misc/matrix.ts | 4 ++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 61d66f49984..06de3d78fd2 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -9,7 +9,7 @@ import { setStyle } from '../util/dom_style'; import { removeFromArray } from '../util/internals'; import { clone } from '../util/lang_object'; import { createCanvasElement } from '../util/misc/dom'; -import { transformPoint } from '../util/misc/matrix'; +import { isIdentityMatrix, transformPoint } from '../util/misc/matrix'; import { Canvas } from '../__types__'; import { TextStyleDeclaration } from './text_style.mixin'; @@ -561,7 +561,8 @@ export abstract class ITextBehaviorMixin< const retinaScaling = this.getCanvasRetinaScaling(); const bbox = this.getBoundingRect(true); const correction = pos.subtract(new Point(bbox.left, bbox.top)); - const offset = correction.add(diff); + const vpt = this.canvas.viewportTransform; + const offset = correction.add(diff).transform(vpt, true); // prepare instance for drag image snapshot by making all non selected text invisible const bgc = this.backgroundColor; const styles = clone(this.styles, true); @@ -573,25 +574,31 @@ export abstract class ITextBehaviorMixin< this.setSelectionStyles(styleOverride, 0, data.selectionStart); this.setSelectionStyles(styleOverride, data.selectionEnd, data.text.length); let dragImage = this.toCanvasElement({ - enableRetinaScaling: enableRetinaScaling, + enableRetinaScaling, }); this.backgroundColor = bgc; this.styles = styles; - // handle retina scaling - if (enableRetinaScaling && retinaScaling > 1) { - const c = createCanvasElement(); - c.width = dragImage.width / retinaScaling; - c.height = dragImage.height / retinaScaling; - const ctx = c.getContext('2d'); + // handle retina scaling and vpt + if (retinaScaling > 1 || !isIdentityMatrix(vpt)) { + const dragImageCanvas = createCanvasElement(); + const size = new Point(dragImage.width, dragImage.height) + .scalarMultiply(retinaScaling) + .transform(vpt, true); + dragImageCanvas.width = size.x; + dragImageCanvas.height = size.y; + const ctx = dragImageCanvas.getContext('2d'); ctx.scale(1 / retinaScaling, 1 / retinaScaling); + const [a, b, c, d] = vpt; + const origin = new Point().transform(vpt, true); + ctx.transform(a, b, c, d, -origin.x, -origin.y); ctx.drawImage(dragImage, 0, 0); - dragImage = c; + dragImage = dragImageCanvas; } this.__dragImageDisposer && this.__dragImageDisposer(); this.__dragImageDisposer = () => { dragImage.remove(); }; - // position drag image offsecreen + // position drag image offscreen setStyle(dragImage, { position: 'absolute', left: -dragImage.width + 'px', diff --git a/src/util/misc/matrix.ts b/src/util/misc/matrix.ts index 2cee6c7aa0c..0699352603e 100644 --- a/src/util/misc/matrix.ts +++ b/src/util/misc/matrix.ts @@ -30,6 +30,10 @@ export type TComposeMatrixArgs = TTranslateMatrixArgs & export type TQrDecomposeOut = Required< Omit >; + +export const isIdentityMatrix = (mat: TMat2D) => + mat.every((value, index) => value === iMatrix[index]); + /** * Apply transform t to point p * @param {Point | IPoint} p The point to transform From 9ec644015bb2128c5d397a9b9c29bd28ffba48e5 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 12:26:51 +0200 Subject: [PATCH 06/52] test `isIdentityMatrix` --- src/util/misc/misc.ts | 2 ++ test/unit/util.js | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/util/misc/misc.ts b/src/util/misc/misc.ts index c5d0835adea..f22b32f3583 100644 --- a/src/util/misc/misc.ts +++ b/src/util/misc/misc.ts @@ -14,6 +14,7 @@ import { getRandomInt, removeFromArray } from '../internals'; import { projectStrokeOnPoints } from './projectStroke'; import { transformPoint, + isIdentityMatrix, invertTransform, composeMatrix, qrDecompose, @@ -116,6 +117,7 @@ fabric.util = { projectStrokeOnPoints, // matrix.ts file transformPoint, + isIdentityMatrix, invertTransform, composeMatrix, qrDecompose, diff --git a/test/unit/util.js b/test/unit/util.js index 72ba5541c37..237cff2983f 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -706,6 +706,11 @@ assert.equal(transform.angle, 30); }); + QUnit.test('isIdentityMatrix', function(assert) { + assert.equal(fabric.util.isIdentityMatrix([1, 0, 0, 1, 0, 0]), true, 'is identity'); + assert.equal(fabric.util.isIdentityMatrix([1, 2, 3, 4, 5, 6]), false, 'is not identity'); + }); + QUnit.test('invertTransform', function(assert) { assert.ok(typeof fabric.util.invertTransform === 'function'); var m1 = [1, 2, 3, 4, 5, 6], m3; From c385df1d2d1d07c3d1db22d6b9f547e9ebfb09e4 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 12:33:09 +0200 Subject: [PATCH 07/52] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac4d6526f03..92614fb8c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- fix(): Draggable Text migration bugs #8534 - chore(TS): Convert Canvas class #8510 - chore(TS): Move object classes #8511 - chore(TS): polish text [#8489](https://github.com/fabricjs/fabric.js/pull/8489) From 057af088ee832d37f5ef03252161fc1dc42a6c5b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 12:55:31 +0200 Subject: [PATCH 08/52] cleanup --- src/mixins/itext_behavior.mixin.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 06de3d78fd2..98db18606a6 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -532,24 +532,19 @@ export abstract class ITextBehaviorMixin< /** * Override to customize the drag image * https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage - * @param {DragEvent} e - * @param {object} data - * @param {number} data.selectionStart - * @param {number} data.selectionEnd - * @param {string} data.text - * @param {string} data.value selected text */ setDragImage( e: DragEvent, - data: { + { + selectionStart, + selectionEnd, + }: { selectionStart: number; selectionEnd: number; - text: string; - value: string; } ) { const flipFactor = new Point(this.flipX ? -1 : 1, this.flipY ? -1 : 1); - const boundaries = this._getCursorBoundaries(data.selectionStart); + const boundaries = this._getCursorBoundaries(selectionStart); const selectionPosition = new Point( boundaries.left + boundaries.leftOffset, boundaries.top + boundaries.topOffset @@ -571,11 +566,12 @@ export abstract class ITextBehaviorMixin< fill: 'transparent', textBackgroundColor: 'transparent', }; - this.setSelectionStyles(styleOverride, 0, data.selectionStart); - this.setSelectionStyles(styleOverride, data.selectionEnd, data.text.length); + this.setSelectionStyles(styleOverride, 0, selectionStart); + this.setSelectionStyles(styleOverride, selectionEnd, this.text.length); let dragImage = this.toCanvasElement({ enableRetinaScaling, }); + // restore values this.backgroundColor = bgc; this.styles = styles; // handle retina scaling and vpt From 7fb83780105c66630edcdcc650a078644fa486a7 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 13:06:26 +0200 Subject: [PATCH 09/52] fix(): acount for stroke style --- src/mixins/itext_behavior.mixin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 98db18606a6..81ead84528d 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -563,6 +563,7 @@ export abstract class ITextBehaviorMixin< const styles = clone(this.styles, true); delete this.backgroundColor; const styleOverride = { + stroke: 'transparent', fill: 'transparent', textBackgroundColor: 'transparent', }; From 7899a25877c33de4260c31bf0cc2ade4aaa947f6 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 13:36:57 +0200 Subject: [PATCH 10/52] drag image visual tests --- test/visual/golden/drag_image.png | Bin 0 -> 8817 bytes test/visual/golden/drag_image_vpt.png | Bin 0 -> 15166 bytes test/visual/text.js | 59 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 test/visual/golden/drag_image.png create mode 100644 test/visual/golden/drag_image_vpt.png diff --git a/test/visual/golden/drag_image.png b/test/visual/golden/drag_image.png new file mode 100644 index 0000000000000000000000000000000000000000..e1122085b0ba571b1e7dd97f23cf55c57aa003b4 GIT binary patch literal 8817 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 literal 0 HcmV?d00001 diff --git a/test/visual/text.js b/test/visual/text.js index cb3bbfe65e5..5edd6fad55a 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -491,5 +491,64 @@ percentage: 0.092, }); + function dragImage({ viewportTransform = fabric.iMatrix, retinaScaling = 1 }, canvas, callback) { + const text = new fabric.Textbox('lorem ipsum\ndolor\nsit Amet2\nconsectgetur', + { objectCaching: false, fontFamily: 'Arial', styles: + {0: {0: {fill: 'red',fontSize: 20},1: {fill: 'red',fontSize: 30},2: {fill: 'red',fontSize: 40},3: {fill: 'red',fontSize: 50},4: {fill: 'red',fontSize: 60},6: {textBackgroundColor: 'yellow'},7: {textBackgroundColor: 'yellow',textDecoration: ' line-through',linethrough: true},8: {textBackgroundColor: 'yellow',textDecoration: ' line-through',linethrough: true},9: {textBackgroundColor: 'yellow'}},1: {0: {textDecoration: 'underline'},1: {textDecoration: 'underline'},2: {fill: 'green',fontStyle: 'italic',textDecoration: 'underline'},3: {fill: 'green',fontStyle: 'italic',textDecoration: 'underline'},4: {fill: 'green',fontStyle: 'italic',textDecoration: 'underline'}},2: {0: {fill: 'blue',fontWeight: 'bold'},1: {fill: 'blue',fontWeight: 'bold'},2: {fill: 'blue',fontWeight: 'bold',fontSize: 63},4: {fontFamily: 'Courier',textDecoration: ' underline',underline: true},5: {fontFamily: 'Courier',textDecoration: ' underline',underline: true},6: {fontFamily: 'Courier',textDecoration: ' overline',overline: true},7: {fontFamily: 'Courier',textDecoration: ' overline',overline: true},8: {fontFamily: 'Courier',textDecoration: ' overline',overline: true}},3: {0: {fill: '#666',textDecoration: 'line-through'},1: {fill: '#666',textDecoration: 'line-through'},2: {fill: '#666',textDecoration: 'line-through'},3: {fill: '#666',textDecoration: 'line-through'},4: {fill: '#666',textDecoration: 'line-through'},7: {textDecoration: ' underline',underline: true},8: {stroke: '#ff1e15',strokeWidth: 2},9: {stroke: '#ff1e15',strokeWidth: 2}}} + } + ); + canvas.add(text); + canvas.viewportTransform = viewportTransform; + const dragEventStub = { + clientX: 0, + clientY: 0, + dataTransfer: { + setDragImage(imageSource, x, y) { + callback(imageSource); + } + } + }; + text.setDragImage(dragEventStub, { + selectionStart: 3, + selectionEnd: 20 + }); + } + + tests.push({ + test: 'Draggable text drag image', + code: dragImage.bind(null, {}), + disabled: fabric.isLikelyNode, + golden: 'drag_image.png', + percentage: 0.03, + fabricClass: 'Canvas' + }); + + tests.push({ + test: 'Draggable text drag image + retina scaling', + code: dragImage.bind(null, { retinaScaling: 3 }), + disabled: fabric.isLikelyNode, + golden: 'drag_image.png', + percentage: 0.03, + fabricClass: 'Canvas' + }); + + tests.push({ + test: 'Draggable text drag image + vpt', + code: dragImage.bind(null, { viewportTransform: [2, 0, 0, 1, 250, -250] }), + disabled: fabric.isLikelyNode, + golden: 'drag_image_vpt.png', + percentage: 0.03, + fabricClass: 'Canvas' + }); + + tests.push({ + test: 'Draggable text drag image + vpt + retina', + code: dragImage.bind(null, { viewportTransform: [2, 0, 0, 1, 250, -250], retinaScaling: 1.25 }), + disabled: fabric.isLikelyNode, + golden: 'drag_image_vpt.png', + percentage: 0.03, + fabricClass: 'Canvas' + }); + tests.forEach(visualTestLoop(QUnit)); })(); From 92cfc437922bb1c19c10f1303fa90f443360b379 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 13:46:31 +0200 Subject: [PATCH 11/52] Update itext_behavior.mixin.ts --- src/mixins/itext_behavior.mixin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 81ead84528d..748abc9ff7f 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -579,7 +579,7 @@ export abstract class ITextBehaviorMixin< if (retinaScaling > 1 || !isIdentityMatrix(vpt)) { const dragImageCanvas = createCanvasElement(); const size = new Point(dragImage.width, dragImage.height) - .scalarMultiply(retinaScaling) + .scalarDivide(retinaScaling) .transform(vpt, true); dragImageCanvas.width = size.x; dragImageCanvas.height = size.y; From 328a01600adbec467a3d5e75925ddbf041c90500 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 22 Dec 2022 14:16:22 +0200 Subject: [PATCH 12/52] better goldens --- test/visual/golden/drag_image.png | Bin 8817 -> 6524 bytes test/visual/golden/drag_image_vpt.png | Bin 15166 -> 11359 bytes test/visual/text.js | 11 ++++++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/test/visual/golden/drag_image.png b/test/visual/golden/drag_image.png index e1122085b0ba571b1e7dd97f23cf55c57aa003b4..a2519187b0b1d90abfeb258f9934374551f23669 100644 GIT binary patch literal 6524 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 diff --git a/test/visual/text.js b/test/visual/text.js index 5edd6fad55a..82e08c8212d 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -504,7 +504,8 @@ clientY: 0, dataTransfer: { setDragImage(imageSource, x, y) { - callback(imageSource); + canvas.getContext().drawImage(imageSource, 0, 0); + callback(canvas.lowerCanvasEl); } } }; @@ -519,6 +520,8 @@ code: dragImage.bind(null, {}), disabled: fabric.isLikelyNode, golden: 'drag_image.png', + width: 120, + height: 220, percentage: 0.03, fabricClass: 'Canvas' }); @@ -528,6 +531,8 @@ code: dragImage.bind(null, { retinaScaling: 3 }), disabled: fabric.isLikelyNode, golden: 'drag_image.png', + width: 110, + height: 250, percentage: 0.03, fabricClass: 'Canvas' }); @@ -537,6 +542,8 @@ code: dragImage.bind(null, { viewportTransform: [2, 0, 0, 1, 250, -250] }), disabled: fabric.isLikelyNode, golden: 'drag_image_vpt.png', + width: 220, + height: 250, percentage: 0.03, fabricClass: 'Canvas' }); @@ -546,6 +553,8 @@ code: dragImage.bind(null, { viewportTransform: [2, 0, 0, 1, 250, -250], retinaScaling: 1.25 }), disabled: fabric.isLikelyNode, golden: 'drag_image_vpt.png', + width: 220, + height: 250, percentage: 0.03, fabricClass: 'Canvas' }); From 9ca3c4f1c5b5f7c271e324d24940d350106ff708 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 3 Jan 2023 23:27:11 +0200 Subject: [PATCH 13/52] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b54262af8..a18711e5af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [next] -- fix(): Draggable Text migration bugs #8534 +- fix(): Draggable Text migration regressions #8534 - fix(Object Stacking): 🔙 refactor logic to support Group 🔝 - chore(TS): migrate Group/ActiveSelection [#8455](https://github.com/fabricjs/fabric.js/pull/8455) - chore(TS): Migrate smaller mixins to classes (dataurl and serialization ) [#8542](https://github.com/fabricjs/fabric.js/pull/8542) From ee30fec76f7173dff50663ca3e1e69488be14802 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 4 Jan 2023 07:31:10 +0200 Subject: [PATCH 14/52] export isIdentityMatrix --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index f496a21aacf..d2306f25a8b 100644 --- a/index.js +++ b/index.js @@ -82,6 +82,7 @@ import { calcDimensionsMatrix, calcRotateMatrix, multiplyTransformMatrices, + isIdentityMatrix, } from './src/util/misc/matrix'; import { stylesFromArray, @@ -190,6 +191,7 @@ const util = { calcDimensionsMatrix, calcRotateMatrix, multiplyTransformMatrices, + isIdentityMatrix, stylesFromArray, stylesToArray, hasStyleChanged, From 1ab5bffc4ef796302ad779e48c95f6c521282807 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 07:33:56 +0200 Subject: [PATCH 15/52] fix(): offset is always 0 --- src/mixins/itext_behavior.mixin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 11659a01d3c..80b8066baf8 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -586,8 +586,7 @@ export abstract class ITextBehaviorMixin< const ctx = dragImageCanvas.getContext('2d'); ctx.scale(1 / retinaScaling, 1 / retinaScaling); const [a, b, c, d] = vpt; - const origin = new Point().transform(vpt, true); - ctx.transform(a, b, c, d, -origin.x, -origin.y); + ctx.transform(a, b, c, d, 0, 0); ctx.drawImage(dragImage, 0, 0); dragImage = dragImageCanvas; } From 3732bff46d73179eb91fc03f791f55046e6dbd01 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 07:51:33 +0200 Subject: [PATCH 16/52] code styling --- src/shapes/text.class.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/shapes/text.class.ts b/src/shapes/text.class.ts index 9635d3a0b75..a1d2d1ef4ec 100644 --- a/src/shapes/text.class.ts +++ b/src/shapes/text.class.ts @@ -1436,9 +1436,8 @@ export class Text< * @private */ _shouldClearDimensionCache() { - let shouldClear = this._forceClearCache; - shouldClear || - (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); + const shouldClear = + this._forceClearCache || this.hasStateChanged('_dimensionAffectingProps'); if (shouldClear) { this.dirty = true; this._forceClearCache = false; From 74c34c9357383dc728bce13c65d1717db271b616 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 09:22:06 +0200 Subject: [PATCH 17/52] fix(): transform event `isClick` --- src/canvas/canvas_events.ts | 20 +++++++++------- test/unit/canvas_events.js | 48 ++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 1f4e5d43a2b..e347e60b004 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -123,6 +123,8 @@ export class Canvas extends SelectableCanvas { */ _previousPointer: Point; + private _isClick: boolean; + /** * Adds mouse listeners to canvas * @private @@ -315,6 +317,7 @@ export class Canvas extends SelectableCanvas { * @param {DragEvent} e */ private _onDragStart(e: DragEvent) { + this._isClick = false; const activeObject = this.getActiveObject(); if ( isFabricObjectWithDragSupport(activeObject) && @@ -766,10 +769,7 @@ export class Canvas extends SelectableCanvas { * @param {Event} e Event object fired on mouseup */ __onMouseUp(e: TPointerEvent) { - const transform = this._currentTransform, - groupSelector = this._groupSelector, - isClick = - !groupSelector || (groupSelector.left === 0 && groupSelector.top === 0); + const transform = this._currentTransform; this._cacheTransformEventData(e); const target = this._target; this._handleEvent(e, 'up:before'); @@ -777,14 +777,14 @@ export class Canvas extends SelectableCanvas { // target undefined will make the _handleEvent search the target if (checkClick(e, RIGHT_CLICK)) { if (this.fireRightClick) { - this._handleEvent(e, 'up', RIGHT_CLICK, isClick); + this._handleEvent(e, 'up', RIGHT_CLICK, this._isClick); } return; } if (checkClick(e, MIDDLE_CLICK)) { if (this.fireMiddleClick) { - this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); + this._handleEvent(e, 'up', MIDDLE_CLICK, this._isClick); } this._resetTransformEventData(); return; @@ -803,7 +803,7 @@ export class Canvas extends SelectableCanvas { this._finalizeCurrentTransform(e); shouldRender = transform.actionPerformed; } - if (!isClick) { + if (!this._isClick) { const targetWasActive = target === this._activeObject; this._maybeGroupObjects(e); if (!shouldRender) { @@ -856,14 +856,14 @@ export class Canvas extends SelectableCanvas { originalMouseUpHandler(e, transform, pointer.x, pointer.y); } this._setCursorFromEvent(e, target); - this._handleEvent(e, 'up', LEFT_CLICK, isClick); + this._handleEvent(e, 'up', LEFT_CLICK, this._isClick); this._groupSelector = null; this._currentTransform = null; // reset the target information about which corner is selected target && (target.__corner = 0); if (shouldRender) { this.requestRenderAll(); - } else if (!isClick) { + } else if (!this._isClick) { this.renderTop(); } } @@ -1054,6 +1054,7 @@ export class Canvas extends SelectableCanvas { * @param {Event} e Event object fired on mousedown */ __onMouseDown(e: TPointerEvent) { + this._isClick = true; this._cacheTransformEventData(e); this._handleEvent(e, 'down:before'); let target: FabricObject | undefined = this._target; @@ -1196,6 +1197,7 @@ export class Canvas extends SelectableCanvas { * @param {Event} e Event object fired on mousemove */ __onMouseMove(e: TPointerEvent) { + this._isClick = false; this._handleEvent(e, 'move:before'); this._cacheTransformEventData(e); diff --git a/test/unit/canvas_events.js b/test/unit/canvas_events.js index c64aaee12a4..906a4560966 100644 --- a/test/unit/canvas_events.js +++ b/test/unit/canvas_events.js @@ -293,7 +293,7 @@ }); - QUnit.test('mouse:up isClick = true', function(assert) { + QUnit.test('mouse:up, isClick = true', function(assert) { var e = { clientX: 30, clientY: 30, which: 1, target: canvas.upperCanvasEl }; var isClick = false; canvas.on('mouse:up', function(opt) { @@ -304,6 +304,39 @@ assert.equal(isClick, true, 'without moving the pointer, the click is true'); }); + QUnit.test('mouse:up after move, isClick = false', function (assert) { + var e = { clientX: 30, clientY: 30, which: 1 }; + var e2 = { clientX: 31, clientY: 31, which: 1 }; + var isClick = true; + canvas.on('mouse:up', function (opt) { + isClick = opt.isClick; + }); + canvas.__onMouseDown(e); + canvas.__onMouseMove(e2); + canvas.__onMouseUp(e2); + assert.equal(isClick, false, 'moving the pointer, the click is false'); + }); + + QUnit.test('mouse:up after dragging, isClick = false', function (assert) { + var e = { clientX: 30, clientY: 30, which: 1 }; + var e2 = { clientX: 31, clientY: 31, which: 1 }; + var isClick = true; + canvas.on('mouse:up', function (opt) { + isClick = opt.isClick; + }); + canvas.__onMouseDown(e); + canvas._onDragStart({ + preventDefault() { + + }, + stopPropagation() { + + } + }); + canvas.__onMouseUp(e2); + assert.equal(isClick, false, 'moving the pointer, the click is false'); + }); + QUnit.test('setDimensions and active brush', function(assert) { var prepareFor = false; var rendered = false; @@ -320,19 +353,6 @@ assert.equal(prepareFor, true, 'the brush called the _setBrushStyles method'); }); - QUnit.test('mouse:up isClick = false', function(assert) { - var e = { clientX: 30, clientY: 30, which: 1 }; - var e2 = { clientX: 31, clientY: 31, which: 1 }; - var isClick = true; - canvas.on('mouse:up', function(opt) { - isClick = opt.isClick; - }); - canvas.__onMouseDown(e); - canvas.__onMouseMove(e2); - canvas.__onMouseUp(e2); - assert.equal(isClick, false, 'moving the pointer, the click is false'); - }); - QUnit.test('mouse:up should return target and currentTarget', function(assert) { var e1 = { clientX: 30, clientY: 30, which: 1 }; var e2 = { clientX: 100, clientY: 100, which: 1 }; From 30c3924b26ec51a2d9c437f82c40d9847afee623 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 09:57:04 +0200 Subject: [PATCH 18/52] fix(): click & drag interactions --- src/canvas/canvas.class.ts | 4 ++++ src/canvas/canvas_events.ts | 8 +++++++- src/mixins/itext_click_behavior.mixin.ts | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/canvas/canvas.class.ts b/src/canvas/canvas.class.ts index 657889199c8..943a7783bca 100644 --- a/src/canvas/canvas.class.ts +++ b/src/canvas/canvas.class.ts @@ -622,6 +622,9 @@ export class SelectableCanvas< this.renderCanvas(this.contextContainer, this._objectsToRender); } + /** + * text selection is rendered by the active text instance during the rendering cycle + */ renderTopLayer(ctx: CanvasRenderingContext2D): void { ctx.save(); if (this.isDrawingMode && this._isCurrentlyDrawing) { @@ -639,6 +642,7 @@ export class SelectableCanvas< /** * Method to render only the top canvas. * Also used to render the group selection box. + * Does not render text selection. */ renderTop() { const ctx = this.contextTop; diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index e347e60b004..5c81906b942 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -863,7 +863,13 @@ export class Canvas extends SelectableCanvas { target && (target.__corner = 0); if (shouldRender) { this.requestRenderAll(); - } else if (!this._isClick) { + } else if ( + !this._isClick && + !( + isInteractiveTextObject(this._activeObject) && + this._activeObject.isEditing + ) + ) { this.renderTop(); } } diff --git a/src/mixins/itext_click_behavior.mixin.ts b/src/mixins/itext_click_behavior.mixin.ts index 4530ced2fd3..048f7800588 100644 --- a/src/mixins/itext_click_behavior.mixin.ts +++ b/src/mixins/itext_click_behavior.mixin.ts @@ -192,7 +192,10 @@ export abstract class ITextClickBehaviorMixin< return; } } - + if (this.__isDragging && options.isClick) { + // false positive drag event, is actually a click + this.setCursorByClick(options.e); + } if (this.__lastSelected && !this.__corner) { this.selected = false; this.__lastSelected = false; From 3af1301f5c7bed210f7d6488654825039f7772cc Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 11:41:45 +0200 Subject: [PATCH 19/52] fix(): false positive drag event --- src/mixins/itext_click_behavior.mixin.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/mixins/itext_click_behavior.mixin.ts b/src/mixins/itext_click_behavior.mixin.ts index 048f7800588..a16470d692d 100644 --- a/src/mixins/itext_click_behavior.mixin.ts +++ b/src/mixins/itext_click_behavior.mixin.ts @@ -173,7 +173,8 @@ export abstract class ITextClickBehaviorMixin< * @private */ mouseUpHandler(options: TransformEvent) { - this.__isMousedown = false; + const shouldSetCursor = this.__isDragging && options.isClick; // false positive drag event, is actually a click + this.__isMousedown = this.__isDragging = false; if ( !this.editable || (this.group && !this.group.interactive) || @@ -192,10 +193,7 @@ export abstract class ITextClickBehaviorMixin< return; } } - if (this.__isDragging && options.isClick) { - // false positive drag event, is actually a click - this.setCursorByClick(options.e); - } + shouldSetCursor && this.setCursorByClick(options.e); if (this.__lastSelected && !this.__corner) { this.selected = false; this.__lastSelected = false; From 7dc98150f728f472aa7588989030d3370a486eba Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 11:44:40 +0200 Subject: [PATCH 20/52] fix(): render selection only if blurred and editing --- src/mixins/itext_key_behavior.mixin.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mixins/itext_key_behavior.mixin.ts b/src/mixins/itext_key_behavior.mixin.ts index 1b6af83e897..a23837e2807 100644 --- a/src/mixins/itext_key_behavior.mixin.ts +++ b/src/mixins/itext_key_behavior.mixin.ts @@ -111,7 +111,12 @@ export abstract class ITextKeyBehaviorMixin< * Override this method to customize cursor behavior on textbox blur */ blur() { - this.abortCursorAnimation(); + if (!this.selected) { + this.abortCursorAnimation(); + // render selection if in editing mode to align to textarea behavior + this.selectionStart !== this.selectionEnd && + this.renderCursorOrSelection(); + } } /** From 1f6d0968f59441c870d85562083969429996eed4 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 11:53:43 +0200 Subject: [PATCH 21/52] fix(): drag start flicker --- src/mixins/itext_behavior.mixin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 80b8066baf8..c60a9094dee 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -247,7 +247,7 @@ export abstract class ITextBehaviorMixin< /** * Aborts cursor animation, clears all timeouts and clear textarea context if necessary */ - abortCursorAnimation() { + abortCursorAnimation(skipClearing = false) { const shouldClear = this._currentTickState || this._currentTickCompleteState; this._currentTickState && this._currentTickState.abort(); @@ -259,7 +259,7 @@ export abstract class ITextBehaviorMixin< this._currentCursorOpacity = 1; // make sure we clear context even if instance is not editing - if (shouldClear) { + if (shouldClear && !skipClearing) { this.clearContextTop(); } } @@ -636,7 +636,7 @@ export abstract class ITextBehaviorMixin< e.dataTransfer.effectAllowed = 'copyMove'; this.setDragImage(e, data); } - this.abortCursorAnimation(); + this.abortCursorAnimation(true); return this.__isDragging; } From 7205724f18b8b8dd71d2a9cbc2af645df5f8e871 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 11:54:46 +0200 Subject: [PATCH 22/52] fix(): export itext while editing fixes flicker while calling `setDragImage` --- src/shapes/itext.class.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/shapes/itext.class.ts b/src/shapes/itext.class.ts index 6a0b5e63520..98c65b31648 100644 --- a/src/shapes/itext.class.ts +++ b/src/shapes/itext.class.ts @@ -278,6 +278,18 @@ export class IText extends ITextClickBehaviorMixin { this.renderCursorOrSelection(); } + /** + * @override block cursor/selection logic while rendering the exported canvas + * @todo this workaround should be replaced with a more robust solution + */ + toCanvasElement(options?: any): HTMLCanvasElement { + const isEditing = this.isEditing; + this.isEditing = false; + const canvas = super.toCanvasElement(options); + this.isEditing = isEditing; + return canvas; + } + /** * Renders cursor or selection (depending on what exists) * it does on the contextTop. If contextTop is not available, do nothing. From acc5b5e8e2235ff12d55de0a84998ce7bd572caa Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 12:09:16 +0200 Subject: [PATCH 23/52] fix(): clear cursor on stale drop target --- src/canvas/canvas_events.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 5c81906b942..6ffbcd15e75 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -111,6 +111,15 @@ export class Canvas extends SelectableCanvas { */ private _dragSource?: FabricObject; + /** + * Holds a reference to an object on the canvas that is the current drop target + * May differ from {@link _draggedoverTarget} + * @todo inspect whether {@link _draggedoverTarget} and {@link _dropTarget} should be merged somehow + * @type FabricObject + * @private + */ + private _dropTarget: FabricObject | undefined; + currentTarget?: FabricObject; currentSubTargets?: FabricObject[]; @@ -347,6 +356,13 @@ export class Canvas extends SelectableCanvas { ) { let dirty = false; const ctx = this.contextTop; + if ( + this._dropTarget && + this._dropTarget !== source && + this._dropTarget !== target + ) { + this._dropTarget.clearContextTop(); + } if (source) { source.clearContextTop(true); source.renderDragSourceEffect(e); @@ -435,7 +451,6 @@ export class Canvas extends SelectableCanvas { // if dragleave is needed, object will not fire dragover so we don't need to trouble ourselves with it this._fireEnterLeaveEvents(target, options); if (target) { - // render drag selection before rendering target cursor for correct visuals if (target.canDrop(e)) { dropTarget = target; } @@ -454,6 +469,7 @@ export class Canvas extends SelectableCanvas { } // render drag effects now that relations between source and target is clear this._renderDragEffects(e, this._dragSource, dropTarget); + this._dropTarget = dropTarget; } /** @@ -487,8 +503,11 @@ export class Canvas extends SelectableCanvas { dragSource: this._dragSource, }; this.fire('dragleave', options); + // fire dragleave on targets this._fireEnterLeaveEvents(undefined, options); + this._renderDragEffects(e, this._dragSource); + this._dropTarget = undefined; // clear targets this.targets = []; this._hoveredTargets = []; From 26aa0146f9a7c4f13ef294d3bb3438f470268e97 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 12:55:33 +0200 Subject: [PATCH 24/52] fix(): _renderDragEffects when overlapping --- src/canvas/canvas_events.ts | 29 ++++++++++++++++++-------- src/shapes/Object/InteractiveObject.ts | 6 +++--- src/shapes/itext.class.ts | 22 ++++++++----------- src/typedefs.ts | 2 ++ 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 6ffbcd15e75..7312f3bb8fa 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -13,6 +13,7 @@ import { Point } from '../point.class'; import { ActiveSelection } from '../shapes/active_selection.class'; import { Group } from '../shapes/group.class'; import type { FabricObject } from '../shapes/Object/FabricObject'; +import { AssertKeys } from '../typedefs'; import { isTouchEvent, stopEvent } from '../util/dom_event'; import { createCanvasElement } from '../util/misc/dom'; import { sendPointToPlane } from '../util/misc/planeChange'; @@ -347,6 +348,9 @@ export class Canvas extends SelectableCanvas { } /** + * First we clear top context where the effects are being rendered. + * Then we render the effects. + * Doing so will render the correct effect for all cases including an overlap between `source` and `target`. * @private */ private _renderDragEffects( @@ -355,26 +359,33 @@ export class Canvas extends SelectableCanvas { target?: FabricObject ) { let dirty = false; - const ctx = this.contextTop; + // clear top context if ( this._dropTarget && this._dropTarget !== source && this._dropTarget !== target ) { this._dropTarget.clearContextTop(); + dirty = true; } + source?.clearContextTop(); + target !== source && target?.clearContextTop(); + // render effects + const ctx = this.contextTop; + ctx.save(); + ctx.transform(...this.viewportTransform); if (source) { - source.clearContextTop(true); - source.renderDragSourceEffect(e); + ctx.save(); + source.transform(ctx); + (source as AssertKeys).renderDragSourceEffect(e); + ctx.restore(); dirty = true; } if (target) { - if (target !== source) { - ctx.restore(); - ctx.save(); - target.clearContextTop(true); - } - target.renderDropTargetEffect(e); + ctx.save(); + target.transform(ctx); + (target as AssertKeys).renderDropTargetEffect(e); + ctx.restore(); dirty = true; } ctx.restore(); diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index 6851bf5edb5..ce9dc02b530 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -1,5 +1,5 @@ import { IPoint, Point } from '../../point.class'; -import type { TCornerPoint, TDegree, TMat2D } from '../../typedefs'; +import type { AssertKeys, TCornerPoint, TDegree, TMat2D } from '../../typedefs'; import { FabricObject } from './Object'; import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; import { @@ -585,7 +585,7 @@ export class InteractiveFabricObject< * @param {DragEvent} e * @returns {boolean} */ - renderDragSourceEffect(e: DragEvent) { + renderDragSourceEffect(this: AssertKeys, e: DragEvent) { // for subclasses } @@ -598,7 +598,7 @@ export class InteractiveFabricObject< * @param {DragEvent} e * @returns {boolean} */ - renderDropTargetEffect(e: DragEvent) { + renderDropTargetEffect(this: AssertKeys, e: DragEvent) { // for subclasses } } diff --git a/src/shapes/itext.class.ts b/src/shapes/itext.class.ts index 98c65b31648..ac9b4267dbf 100644 --- a/src/shapes/itext.class.ts +++ b/src/shapes/itext.class.ts @@ -8,7 +8,8 @@ import { keysMapRtl, } from '../mixins/itext_key_const'; import { classRegistry } from '../util/class_registry'; -import { TClassProperties, TFiller } from '../typedefs'; +import { AssertKeys, TClassProperties, TFiller } from '../typedefs'; +import type { Canvas } from '../canvas/canvas_events'; export type ITextEvents = ObjectEvents & { 'selection:changed': never; @@ -470,20 +471,15 @@ export class IText extends ITextClickBehaviorMixin { /** * Renders drag start text selection */ - renderDragSourceEffect() { - if (this.__isDragging && this.__dragStartSelection) { - this._renderSelection( - this.canvas.contextTop, - this.__dragStartSelection, - this._getCursorBoundaries( - this.__dragStartSelection.selectionStart, - true - ) - ); - } + renderDragSourceEffect(this: AssertKeys) { + this._renderSelection( + this.canvas.contextTop, + this.__dragStartSelection, + this._getCursorBoundaries(this.__dragStartSelection.selectionStart, true) + ); } - renderDropTargetEffect(e) { + renderDropTargetEffect(e: DragEvent) { const dragSelection = this.getSelectionStartFromPointer(e); this.renderCursorAt(dragSelection); } diff --git a/src/typedefs.ts b/src/typedefs.ts index 50c1369bae8..d4fdce9fdaa 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -114,3 +114,5 @@ export type TDataUrlOptions = TToCanvasElementOptions & { quality?: number; enableRetinaScaling?: boolean; }; + +export type AssertKeys = T & Record>; From 3a68c7c7206c1902a3065d201dcccaf65cc547ca Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 14:02:39 +0200 Subject: [PATCH 25/52] fix(): find drag targets --- src/canvas/canvas_events.ts | 89 +++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 7312f3bb8fa..202e9419c45 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -437,6 +437,22 @@ export class Canvas extends SelectableCanvas { this._dragSource && this._dragSource.fire('drag', options); } + /** + * As opposed to {@link findTarget} we want the top most object to be returned w/o the active object cutting in line. + * Override at will + */ + protected findDragTargets(e: DragEvent) { + this.targets = []; + const target = this._searchPossibleTargets( + this._objects, + this.getPointer(e, true) + ); + return { + target, + targets: [...this.targets], + }; + } + /** * prevent default to allow drop event to be fired * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#specifying_drop_targets @@ -444,17 +460,17 @@ export class Canvas extends SelectableCanvas { * @param {DragEvent} [e] Event object fired on Event.js shake */ private _onDragOver(e: DragEvent) { - const eventType = 'dragover', - target = this.findTarget(e), - targets = this.targets, - options = { - e: e, - target, - subTargets: targets, - dragSource: this._dragSource as FabricObject, - canDrop: false, - dropTarget: undefined, - }; + const eventType = 'dragover'; + const { target, targets } = this.findDragTargets(e); + const dragSource = this._dragSource as FabricObject; + const options = { + e: e, + target, + subTargets: targets, + dragSource, + canDrop: false, + dropTarget: undefined, + }; let dropTarget; // fire on canvas this.fire(eventType, options); @@ -479,7 +495,7 @@ export class Canvas extends SelectableCanvas { subTarget.fire(eventType, options); } // render drag effects now that relations between source and target is clear - this._renderDragEffects(e, this._dragSource, dropTarget); + this._renderDragEffects(e, dragSource, dropTarget); this._dropTarget = dropTarget; } @@ -489,11 +505,11 @@ export class Canvas extends SelectableCanvas { * @param {Event} [e] Event object fired on Event.js shake */ private _onDragEnter(e: DragEvent) { - const target = this.findTarget(e); + const { target, targets } = this.findDragTargets(e); const options = { e, - target: target as FabricObject, - subTargets: this.targets, + target, + subTargets: targets, dragSource: this._dragSource, }; this.fire('dragenter', options); @@ -533,8 +549,11 @@ export class Canvas extends SelectableCanvas { * @param {Event} e */ private _onDrop(e: DragEvent) { - const options = this._simpleEventHandler('drop:before', { + const { target, targets } = this.findDragTargets(e); + const options = this._basicEventHandler('drop:before', { e, + target, + subTargets: targets, dragSource: this._dragSource, pointer: this.getPointer(e), }); @@ -555,7 +574,13 @@ export class Canvas extends SelectableCanvas { * @param {Event} e Event object fired on mousedown */ private _onContextMenu(e: TPointerEvent): false { - const options = this._simpleEventHandler('contextmenu:before', { e }); + const target = this.findTarget(e), + subTargets = this.targets || []; + const options = this._basicEventHandler('contextmenu:before', { + e, + target, + subTargets, + }); // TODO: this line is silly because the dev can subscribe to the event and prevent it themselves this.stopContextMenu && stopEvent(e); this._basicEventHandler('contextmenu', options); @@ -904,36 +929,6 @@ export class Canvas extends SelectableCanvas { } } - /** - * @private - * Handle event firing for target and subtargets - * @param {String} eventType event to fire (up, down or move) - * @param {Event} e event from mouse - * @param {object} [data] event data overrides - * @return {object} options - */ - _simpleEventHandler< - T extends keyof (CanvasEvents | ObjectEvents), - E extends TPointerEvent | DragEvent - >( - eventType: T, - { - e, - ...data - }: Omit<(CanvasEvents & ObjectEvents)[T], 'target' | 'subTargets'> & - TEvent - ) { - const target = this.findTarget(e), - subTargets = this.targets || []; - // @ts-expect-error TODO fix generic e - return this._basicEventHandler(eventType, { - e, - target, - subTargets, - ...data, - }); - } - _basicEventHandler( eventType: T, options: (CanvasEvents & ObjectEvents)[T] From 8b7971fd99f01d2873674a86e27bb36d7152c67b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 14:07:25 +0200 Subject: [PATCH 26/52] cleanup --- src/canvas/canvas_events.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 202e9419c45..69bafe5d769 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -827,19 +827,20 @@ export class Canvas extends SelectableCanvas { const transform = this._currentTransform; this._cacheTransformEventData(e); const target = this._target; + const isClick = this._isClick; this._handleEvent(e, 'up:before'); // if right/middle click just fire events and return // target undefined will make the _handleEvent search the target if (checkClick(e, RIGHT_CLICK)) { if (this.fireRightClick) { - this._handleEvent(e, 'up', RIGHT_CLICK, this._isClick); + this._handleEvent(e, 'up', RIGHT_CLICK, isClick); } return; } if (checkClick(e, MIDDLE_CLICK)) { if (this.fireMiddleClick) { - this._handleEvent(e, 'up', MIDDLE_CLICK, this._isClick); + this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); } this._resetTransformEventData(); return; @@ -858,7 +859,7 @@ export class Canvas extends SelectableCanvas { this._finalizeCurrentTransform(e); shouldRender = transform.actionPerformed; } - if (!this._isClick) { + if (!isClick) { const targetWasActive = target === this._activeObject; this._maybeGroupObjects(e); if (!shouldRender) { @@ -911,7 +912,7 @@ export class Canvas extends SelectableCanvas { originalMouseUpHandler(e, transform, pointer.x, pointer.y); } this._setCursorFromEvent(e, target); - this._handleEvent(e, 'up', LEFT_CLICK, this._isClick); + this._handleEvent(e, 'up', LEFT_CLICK, isClick); this._groupSelector = null; this._currentTransform = null; // reset the target information about which corner is selected @@ -919,7 +920,7 @@ export class Canvas extends SelectableCanvas { if (shouldRender) { this.requestRenderAll(); } else if ( - !this._isClick && + !isClick && !( isInteractiveTextObject(this._activeObject) && this._activeObject.isEditing From 921b5f2734ec81e9620d61947d01b06eb0fd161b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 9 Jan 2023 14:18:03 +0200 Subject: [PATCH 27/52] Update EventTypeDefs.ts --- src/EventTypeDefs.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index 32f79deba2b..9716ac7c1a8 100644 --- a/src/EventTypeDefs.ts +++ b/src/EventTypeDefs.ts @@ -134,10 +134,10 @@ export type TPointerEventInfo = currentTarget?: FabricObject | null; }; -type SimpleEventHandler = - TEventWithTarget & { - subTargets: FabricObject[]; - }; +type SimpleEventHandler = TEvent & { + target?: FabricObject; + subTargets: FabricObject[]; +}; type InEvent = { previousTarget?: FabricObject; From 0c0e853c995a08740f9f37064293910a3c42c151 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 09:18:32 +0200 Subject: [PATCH 28/52] Update .gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 76d31bfc104..8e14a7739bf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ change-output.md before_commit /coverage/ .idea/ -/dist/fabric.require.js -/dist/fabric.min.js.gz +/dist /cli_output/ From 37f72580cfaef36893e094391eb41250ec8afb20 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 09:48:50 +0200 Subject: [PATCH 29/52] fix imports --- src/shapes/itext.class.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/itext.class.ts b/src/shapes/itext.class.ts index 30dc261d7a8..d7b032efe5b 100644 --- a/src/shapes/itext.class.ts +++ b/src/shapes/itext.class.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import type { Canvas } from '../canvas/canvas_events'; +import { Canvas } from '../canvas/canvas_events'; import { ObjectEvents, TPointerEventInfo } from '../EventTypeDefs'; import { ITextClickBehaviorMixin } from '../mixins/itext_click_behavior.mixin'; import { From f4b03a9c14046e481ef9596cdc972abaff4d4896 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 09:48:59 +0200 Subject: [PATCH 30/52] fix merge --- src/mixins/itext_behavior.mixin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index b13adddee4e..a082ec05654 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -540,7 +540,7 @@ export abstract class ITextBehaviorMixin< e.dataTransfer.effectAllowed = 'copyMove'; this.setDragImage(e, data); } - this.abortCursorAnimation(true); + this.abortCursorAnimation(); return this.__isDragging; } From ced0b675e4e7144d8af254e1fd7987efdd918054 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 09:51:21 +0200 Subject: [PATCH 31/52] revert --- src/mixins/itext_key_behavior.mixin.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/mixins/itext_key_behavior.mixin.ts b/src/mixins/itext_key_behavior.mixin.ts index ed7b6427503..feff5723e59 100644 --- a/src/mixins/itext_key_behavior.mixin.ts +++ b/src/mixins/itext_key_behavior.mixin.ts @@ -111,12 +111,7 @@ export abstract class ITextKeyBehaviorMixin< * Override this method to customize cursor behavior on textbox blur */ blur() { - if (!this.selected) { - this.abortCursorAnimation(); - // render selection if in editing mode to align to textarea behavior - this.selectionStart !== this.selectionEnd && - this.renderCursorOrSelection(); - } + this.abortCursorAnimation(); } /** From 9c41e186cba78caaf51c253034a842a987f46434 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 10:05:00 +0200 Subject: [PATCH 32/52] types --- src/EventTypeDefs.ts | 2 +- src/mixins/itext_behavior.mixin.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index 9716ac7c1a8..db70959a9d1 100644 --- a/src/EventTypeDefs.ts +++ b/src/EventTypeDefs.ts @@ -156,7 +156,7 @@ export type DragEventData = TEvent & { dropTarget?: FabricObject; }; -type DropEventData = DragEventData & { pointer: Point }; +export type DropEventData = DragEventData & { pointer: Point }; type DnDEvents = { dragstart: TEventWithTarget; diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index a082ec05654..b6e7bc1fe9e 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -1,7 +1,13 @@ // @ts-nocheck import { getEnv } from '../env'; -import { ObjectEvents, TEvent, TPointerEvent } from '../EventTypeDefs'; +import { + DragEventData, + DropEventData, + ObjectEvents, + TEvent, + TPointerEvent, +} from '../EventTypeDefs'; import { Point } from '../point.class'; import type { FabricObject } from '../shapes/Object/Object'; import { Text } from '../shapes/text.class'; @@ -573,7 +579,7 @@ export abstract class ITextBehaviorMixin< * @param {object} options * @param {DragEvent} options.e */ - dragEnterHandler({ e }: TEvent) { + dragEnterHandler({ e }: DragEventData) { const canDrop = !e.defaultPrevented && this.canDrop(e); if (!this.__isDraggingOver && canDrop) { this.__isDraggingOver = true; @@ -586,7 +592,7 @@ export abstract class ITextBehaviorMixin< * @param {object} options * @param {DragEvent} options.e */ - dragOverHandler(ev: TEvent) { + dragOverHandler(ev: DragEventData) { const { e } = ev; const canDrop = !e.defaultPrevented && this.canDrop(e); if (!this.__isDraggingOver && canDrop) { @@ -624,7 +630,7 @@ export abstract class ITextBehaviorMixin< * @param {object} options * @param {DragEvent} options.e */ - dragEndHandler({ e }: TEvent) { + dragEndHandler({ e }: DragEventData) { if (this.__isDragging && this.__dragStartFired) { // once the drop event finishes we check if we need to change the drag source // if the drag source received the drop we bail out @@ -671,7 +677,7 @@ export abstract class ITextBehaviorMixin< * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#performing_a_drop * @private */ - dropHandler(ev: TEvent) { + dropHandler(ev: DropEventData) { const { e } = ev; const didDrop = e.defaultPrevented; this.__isDraggingOver = false; From 4d84e357198fc53dbbde0712b720256e65cc25cf Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 10:18:39 +0200 Subject: [PATCH 33/52] Update canvas_events.ts --- src/canvas/canvas_events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 6c070b64aeb..e13cdf03286 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -466,7 +466,7 @@ export class Canvas extends SelectableCanvas { const { target, targets } = this.findDragTargets(e); const dragSource = this._dragSource as FabricObject; const options = { - e: e, + e, target, subTargets: targets, dragSource, From 8a88befa38497de82e58a06ed8d3899580c2adb3 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 10:43:51 +0200 Subject: [PATCH 34/52] Update text.js --- test/visual/text.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/visual/text.js b/test/visual/text.js index 1f5760f983c..336b0e913c4 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -518,7 +518,7 @@ tests.push({ test: 'Draggable text drag image', code: dragImage.bind(null, {}), - disabled: fabric.isLikelyNode, + disabled: fabric.getEnv().isLikelyNode, golden: 'drag_image.png', width: 120, height: 220, @@ -529,7 +529,7 @@ tests.push({ test: 'Draggable text drag image + retina scaling', code: dragImage.bind(null, { retinaScaling: 3 }), - disabled: fabric.isLikelyNode, + disabled: fabric.getEnv().isLikelyNode, golden: 'drag_image.png', width: 110, height: 250, @@ -540,7 +540,7 @@ tests.push({ test: 'Draggable text drag image + vpt', code: dragImage.bind(null, { viewportTransform: [2, 0, 0, 1, 250, -250] }), - disabled: fabric.isLikelyNode, + disabled: fabric.getEnv().isLikelyNode, golden: 'drag_image_vpt.png', width: 220, height: 250, @@ -551,7 +551,7 @@ tests.push({ test: 'Draggable text drag image + vpt + retina', code: dragImage.bind(null, { viewportTransform: [2, 0, 0, 1, 250, -250], retinaScaling: 1.25 }), - disabled: fabric.isLikelyNode, + disabled: fabric.getEnv().isLikelyNode, golden: 'drag_image_vpt.png', width: 220, height: 250, From 542f8c697e20bd303facf6fa6619c299b9d66032 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 14:21:20 +0200 Subject: [PATCH 35/52] Update itext_click_behaviour.js --- test/unit/itext_click_behaviour.js | 75 +++++++++++++++--------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/test/unit/itext_click_behaviour.js b/test/unit/itext_click_behaviour.js index d3e244b5a76..f015da89319 100644 --- a/test/unit/itext_click_behaviour.js +++ b/test/unit/itext_click_behaviour.js @@ -1,11 +1,12 @@ (function(){ - var canvas; QUnit.module('iText click interaction', function (hooks) { - hooks.beforeEach(function () { + var canvas; + hooks.before(function () { canvas = new fabric.Canvas(null, { enableRetinaScaling: false }); }); + hooks.after(() => canvas.dispose()); hooks.afterEach(function () { canvas.clear(); canvas.cancelRequestedRender(); @@ -276,47 +277,47 @@ canvas.renderAll(); }); - QUnit.module('iText click interaction', function (hooks) { - let enableRetinaScaling = false, canvas, eventData; - hooks.before(function () { - fabric.config.configure({ devicePixelRatio: 2 }); - }); - hooks.after(function () { - fabric.config.restoreDefaults(); - }); - hooks.beforeEach(function () { - canvas = new fabric.Canvas(null, { - enableRetinaScaling, + [true, false].forEach(enableRetinaScaling => { + QUnit.module(`enableRetinaScaling = ${enableRetinaScaling}`, function (hooks) { + let canvas, eventData, iText; + let count = 0, countCanvas = 0; + hooks.before(() => { + fabric.config.configure({ devicePixelRatio: 2 }); }); - eventData = { - which: 1, - target: canvas.upperCanvasEl, - ...(enableRetinaScaling ? { - clientX: 60, - clientY: 30 - } : { - clientX: 30, - clientY: 10 - }) - }; - }); - hooks.afterEach(() => canvas.dispose()); - - [true, false].forEach(shouldScale => { - enableRetinaScaling = shouldScale; - QUnit.test(`click on editing itext make selection:changed fire (enableRetinaScaling = ${enableRetinaScaling})`, function (assert) { - var done = assert.async(); - var count = 0; - var countCanvas = 0; - var iText = new fabric.IText('test test'); - canvas.on('text:selection:changed', function () { + hooks.after(() => { + fabric.config.restoreDefaults(); + }); + hooks.beforeEach(() => { + canvas = new fabric.Canvas(null, { + enableRetinaScaling, + }); + eventData = { + which: 1, + target: canvas.upperCanvasEl, + ...(enableRetinaScaling ? { + clientX: 60, + clientY: 30 + } : { + clientX: 30, + clientY: 15 + }) + }; + count = 0; + countCanvas = 0; + iText = new fabric.IText('test test'); + canvas.add(iText); + canvas.on('text:selection:changed', () => { countCanvas++; }); - iText.on('selection:changed', function () { + iText.on('selection:changed', () => { count++; }); + }); + hooks.afterEach(() => canvas.dispose()); + + QUnit.test(`click on editing itext make selection:changed fire`, function (assert) { + var done = assert.async(); assert.equal(canvas._isRetinaScaling(), enableRetinaScaling, 'test state is correct'); - canvas.add(iText); assert.equal(canvas.getActiveObject(), null, 'no active object exist'); assert.equal(count, 0, 'no selection:changed fired yet'); assert.equal(countCanvas, 0, 'no text:selection:changed fired yet'); From e6dd51ad75c756030e5e26b4d54cd1267123a76b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 14:24:13 +0200 Subject: [PATCH 36/52] init --- test/unit/draggable_text.js | 183 ++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 test/unit/draggable_text.js diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js new file mode 100644 index 00000000000..55c4b2800b5 --- /dev/null +++ b/test/unit/draggable_text.js @@ -0,0 +1,183 @@ +QUnit.module('draggable text', function (hooks) { + let canvas; + hooks.before(function () { + canvas = new fabric.Canvas(null, { + enableRetinaScaling: false + }); + }); + hooks.after(() => canvas.dispose()); + hooks.afterEach(function () { + canvas.clear(); + canvas.cancelRequestedRender(); + }); + + function assertCursorAnimation(assert, text, active = false) { + const cursorState = [text._currentTickState, text._currentTickCompleteState].some( + (cursorAnimation) => cursorAnimation && !cursorAnimation.isDone() + ); + assert.equal(cursorState, active, `cursor animation state should be ${active}`); + } + + [true, false].forEach(enableRetinaScaling => { + QUnit.module(`enableRetinaScaling = ${enableRetinaScaling}`, function (hooks) { + let canvas, eventData, iText, iText2; + let count = 0, countCanvas = 0; + hooks.before(() => { + fabric.config.configure({ devicePixelRatio: 2 }); + }); + hooks.after(() => { + fabric.config.restoreDefaults(); + }); + hooks.beforeEach(() => { + canvas = new fabric.Canvas(null, { + enableRetinaScaling, + }); + eventData = { + which: 1, + target: canvas.upperCanvasEl, + preventDefault() { + this.defaultPrevented = true; + }, + stopPropagation() { + this.propagationStopped = true; + }, + dataTransfer: { + data: {}, + setData(type, value) { + this.data[type] = value; + }, + setDragImage(img, x, y) { + this.dragImageData = { img, x, y }; + }, + }, + ...(enableRetinaScaling ? { + clientX: 60, + clientY: 30 + } : { + clientX: 30, + clientY: 15 + }) + }; + count = 0; + countCanvas = 0; + iText = new fabric.IText('test test'); + iText2 = new fabric.IText('test2 test2', { left: 200 }); + canvas.add(iText, iText2); + canvas.on('text:selection:changed', () => { + countCanvas++; + }); + iText.on('selection:changed', () => { + count++; + }); + }); + hooks.afterEach(() => canvas.dispose()); + + function startDragging() { + + } + + QUnit.test('draggable: click', function (assert) { + iText.enterEditing(); + iText.selectionStart = 0; + iText.selectionEnd = 4; + assert.equal(count, 1, 'selection:changed fired'); + assert.equal(countCanvas, 1, 'text:selection:changed fired'); + let called = 0; + // sinon spy!! + // iText.setCursorByClick = () => called++; + iText.fire('mousedown:before', { + e: eventData, + pointer: canvas.getPointer(eventData, true) + }); + assert.ok(iText.__isDragging); + iText.fire('mousedown', { + e: eventData, + pointer: canvas.getPointer(eventData, true) + }); + assert.ok(iText.__isDragging, 'flagged as dragging'); + // assert.equal(called, 0, 'should not set cursor on mouse up'); + iText.fire('mouseup', { + e: eventData, + pointer: canvas.getPointer(eventData, true), + isClick: true + }); + assert.ok(!iText.__isDragging, 'unflagged as dragging'); + // assert.equal(called, 1, 'should set cursor on mouse up'); + assert.equal(iText.selectionStart, 2, 'Itext set the selectionStart'); + assert.equal(iText.selectionEnd, 2, 'Itext set the selectionend'); + assertCursorAnimation(assert, iText, true); + assert.equal(count, 2, 'selection:changed fired'); + assert.equal(countCanvas, 2, 'text:selection:changed fired'); + }); + + QUnit.test('drag start', function (assert) { + canvas.setActiveObject(iText); + const renderEffects = []; + canvas._renderDragEffects = (e, source, target) => renderEffects.push({ e, source, target }); + iText.enterEditing(); + iText.selectionStart = 0; + iText.selectionEnd = 4; + assert.equal(count, 1, 'selection:changed fired'); + assert.equal(countCanvas, 1, 'text:selection:changed fired'); + iText.fire('mousedown:before', { + e: eventData, + pointer: canvas.getPointer(eventData, true) + }); + assert.ok(iText.__isDragging); + iText.fire('mousedown', { + e: eventData, + pointer: canvas.getPointer(eventData, true) + }); + const eventStream = { + canvas: [], + source: [], + target: [], + }; + ['dragstart', 'dragover', 'drag', 'dragenter', 'dragleave', 'drop', 'dragend'].forEach(type => { + canvas.on(type, (ev) => { + eventStream.canvas.push({ ...ev, type }); + }); + iText.on(type, (ev) => { + eventStream.source.push({ ...ev, type }); + }); + iText2.on(type, (ev) => { + eventStream.target.push({ ...ev, type }); + }); + }); + + canvas._onDragStart(eventData); + const charStyle = { "stroke": null, "strokeWidth": 1, "fill": "rgb(0,0,0)", "fontFamily": "Times New Roman", "fontSize": 40, "fontWeight": "normal", "fontStyle": "normal", "underline": false, "overline": false, "linethrough": false, "deltaY": 0, "textBackgroundColor": "" }; + assert.deepEqual(eventData.dataTransfer.data, { + 'application/fabric': JSON.stringify({ + value: 'test', + styles: [charStyle, charStyle, charStyle, charStyle] + }), + 'text/plain': "test" + }, 'should set dataTransfer'); + assert.equal(eventData.dataTransfer.effectAllowed, 'copyMove', 'should set effectAllowed'); + // assert.equal(eventData.defaultPrevented, true, 'drag event default prevented'); + assert.ok(eventData.dataTransfer.dragImageData.img instanceof HTMLCanvasElement, 'drag image was set'); + assert.equal(eventData.dataTransfer.dragImageData.x, 30, 'drag image position'); + assert.equal(eventData.dataTransfer.dragImageData.y, 15, 'drag image position'); + assert.deepEqual(eventStream, { + canvas: [{ target: iText, e: eventData }], + source: [{ target: iText, e: eventData }], + target: [], + }, 'drag start fired'); + assert.deepEqual(renderEffects, [], 'not rendered effects yet'); + const dragEvent = { ...eventData, defaultPrevented: false }; + for (let index = 0; index < 350; index++) { + canvas._onDragOver({ + ...eventData, + defaultPrevented: false, + clientX: eventData.clientX + index * (enableRetinaScaling ? canvas._getRetinaScaling() : 1) + }); + } + // canvas._onDrop(dragEvent); + canvas._onDragEnd(dragEvent); + console.log(eventStream) + // assert.deepEqual(renderEffects, [{ e: eventData, source: iText, target: iText }]); + }); + }); + }); +}); \ No newline at end of file From da6fc7ddb1b74f4193ca361fb2530e352d9f9cd3 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 14:57:16 +0200 Subject: [PATCH 37/52] init tests --- test/unit/draggable_text.js | 146 +++++++++++++++++------------------- 1 file changed, 69 insertions(+), 77 deletions(-) diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index 55c4b2800b5..359e5d9044c 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -1,4 +1,4 @@ -QUnit.module('draggable text', function (hooks) { +(fabric.getEnv().isLikelyNode ? QUnit.module.skip : QUnit.module)('draggable text', function (hooks) { let canvas; hooks.before(function () { canvas = new fabric.Canvas(null, { @@ -20,7 +20,7 @@ QUnit.module('draggable text', function (hooks) { [true, false].forEach(enableRetinaScaling => { QUnit.module(`enableRetinaScaling = ${enableRetinaScaling}`, function (hooks) { - let canvas, eventData, iText, iText2; + let canvas, eventData, iText, iText2, eventStream, renderEffects; let count = 0, countCanvas = 0; hooks.before(() => { fabric.config.configure({ devicePixelRatio: 2 }); @@ -43,6 +43,7 @@ QUnit.module('draggable text', function (hooks) { }, dataTransfer: { data: {}, + dropEffect: 'none', setData(type, value) { this.data[type] = value; }, @@ -58,114 +59,105 @@ QUnit.module('draggable text', function (hooks) { clientY: 15 }) }; - count = 0; - countCanvas = 0; iText = new fabric.IText('test test'); iText2 = new fabric.IText('test2 test2', { left: 200 }); canvas.add(iText, iText2); + canvas.setActiveObject(iText); + iText.enterEditing(); + iText.selectionStart = 0; + iText.selectionEnd = 4; + count = 0; + countCanvas = 0; canvas.on('text:selection:changed', () => { countCanvas++; }); iText.on('selection:changed', () => { count++; }); + eventStream = { + canvas: [], + source: [], + target: [], + }; + ['dragstart', 'dragover', 'drag', 'dragenter', 'dragleave', 'drop', 'dragend'].forEach(type => { + canvas.on(type, (ev) => { + eventStream.canvas.push({ ...ev, type }); + }); + iText.on(type, (ev) => { + eventStream.source.push({ ...ev, type }); + }); + iText2.on(type, (ev) => { + eventStream.target.push({ ...ev, type }); + }); + }); + renderEffects = []; + canvas._renderDragEffects = (e, source, target) => renderEffects.push({ e, source, target }); }); hooks.afterEach(() => canvas.dispose()); - - function startDragging() { - + + function startDragging(eventData) { + const e = { ...eventData }; + canvas._onMouseDown({ ...eventData }); + canvas._onDragStart(e); + return e; } - QUnit.test('draggable: click', function (assert) { - iText.enterEditing(); - iText.selectionStart = 0; - iText.selectionEnd = 4; - assert.equal(count, 1, 'selection:changed fired'); - assert.equal(countCanvas, 1, 'text:selection:changed fired'); + QUnit.test('click sets cursor', function (assert) { + assert.equal(count, 0, 'selection:changed fired'); + assert.equal(countCanvas, 0, 'text:selection:changed fired'); let called = 0; // sinon spy!! // iText.setCursorByClick = () => called++; - iText.fire('mousedown:before', { - e: eventData, - pointer: canvas.getPointer(eventData, true) - }); - assert.ok(iText.__isDragging); - iText.fire('mousedown', { - e: eventData, - pointer: canvas.getPointer(eventData, true) - }); + canvas._onMouseDown(eventData); assert.ok(iText.__isDragging, 'flagged as dragging'); // assert.equal(called, 0, 'should not set cursor on mouse up'); - iText.fire('mouseup', { - e: eventData, - pointer: canvas.getPointer(eventData, true), - isClick: true - }); + canvas._onMouseUp(eventData); assert.ok(!iText.__isDragging, 'unflagged as dragging'); // assert.equal(called, 1, 'should set cursor on mouse up'); assert.equal(iText.selectionStart, 2, 'Itext set the selectionStart'); assert.equal(iText.selectionEnd, 2, 'Itext set the selectionend'); assertCursorAnimation(assert, iText, true); - assert.equal(count, 2, 'selection:changed fired'); - assert.equal(countCanvas, 2, 'text:selection:changed fired'); + assert.equal(count, 1, 'selection:changed fired'); + assert.equal(countCanvas, 1, 'text:selection:changed fired'); }); QUnit.test('drag start', function (assert) { - canvas.setActiveObject(iText); - const renderEffects = []; - canvas._renderDragEffects = (e, source, target) => renderEffects.push({ e, source, target }); - iText.enterEditing(); - iText.selectionStart = 0; - iText.selectionEnd = 4; - assert.equal(count, 1, 'selection:changed fired'); - assert.equal(countCanvas, 1, 'text:selection:changed fired'); - iText.fire('mousedown:before', { - e: eventData, - pointer: canvas.getPointer(eventData, true) - }); - assert.ok(iText.__isDragging); - iText.fire('mousedown', { - e: eventData, - pointer: canvas.getPointer(eventData, true) - }); - const eventStream = { - canvas: [], - source: [], - target: [], - }; - ['dragstart', 'dragover', 'drag', 'dragenter', 'dragleave', 'drop', 'dragend'].forEach(type => { - canvas.on(type, (ev) => { - eventStream.canvas.push({ ...ev, type }); - }); - iText.on(type, (ev) => { - eventStream.source.push({ ...ev, type }); - }); - iText2.on(type, (ev) => { - eventStream.target.push({ ...ev, type }); - }); - }); - - canvas._onDragStart(eventData); + const e = startDragging(eventData); const charStyle = { "stroke": null, "strokeWidth": 1, "fill": "rgb(0,0,0)", "fontFamily": "Times New Roman", "fontSize": 40, "fontWeight": "normal", "fontStyle": "normal", "underline": false, "overline": false, "linethrough": false, "deltaY": 0, "textBackgroundColor": "" }; - assert.deepEqual(eventData.dataTransfer.data, { + assert.deepEqual(e.dataTransfer.data, { 'application/fabric': JSON.stringify({ value: 'test', styles: [charStyle, charStyle, charStyle, charStyle] }), 'text/plain': "test" }, 'should set dataTransfer'); - assert.equal(eventData.dataTransfer.effectAllowed, 'copyMove', 'should set effectAllowed'); + assert.equal(e.dataTransfer.effectAllowed, 'copyMove', 'should set effectAllowed'); // assert.equal(eventData.defaultPrevented, true, 'drag event default prevented'); - assert.ok(eventData.dataTransfer.dragImageData.img instanceof HTMLCanvasElement, 'drag image was set'); - assert.equal(eventData.dataTransfer.dragImageData.x, 30, 'drag image position'); - assert.equal(eventData.dataTransfer.dragImageData.y, 15, 'drag image position'); - assert.deepEqual(eventStream, { - canvas: [{ target: iText, e: eventData }], - source: [{ target: iText, e: eventData }], - target: [], - }, 'drag start fired'); + assert.ok(e.dataTransfer.dragImageData.img instanceof HTMLCanvasElement, 'drag image was set'); + assert.equal(e.dataTransfer.dragImageData.x, 30, 'drag image position'); + assert.equal(e.dataTransfer.dragImageData.y, 15, 'drag image position'); assert.deepEqual(renderEffects, [], 'not rendered effects yet'); - const dragEvent = { ...eventData, defaultPrevented: false }; + canvas._onDragEnd(eventData); + assert.deepEqual(eventStream.source, [ + { + e, + target: iText, + type: 'dragstart' + }, { + e: eventData, + target: iText, + type: 'dragend', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + didDrop: false + } + ], 'events should match'); + assert.deepEqual(eventStream.canvas, eventStream.source, 'events should match'); + }); + + QUnit.test.skip('drag over', function (assert) { + const e = startDragging(eventData); for (let index = 0; index < 350; index++) { canvas._onDragOver({ ...eventData, @@ -174,10 +166,10 @@ QUnit.module('draggable text', function (hooks) { }); } // canvas._onDrop(dragEvent); - canvas._onDragEnd(dragEvent); + // canvas._onDragEnd(dragEvent); console.log(eventStream) // assert.deepEqual(renderEffects, [{ e: eventData, source: iText, target: iText }]); - }); + }) }); }); }); \ No newline at end of file From 360ad4d17752248d056e997e203094e680ff4bf0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 12 Jan 2023 15:01:43 +0200 Subject: [PATCH 38/52] Update itext_behavior.mixin.ts --- src/mixins/itext_behavior.mixin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index b6e7bc1fe9e..bc70316b347 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -607,7 +607,6 @@ export abstract class ITextBehaviorMixin< // inform event subscribers ev.canDrop = true; ev.dropTarget = this; - // find cursor under the drag part. } } From 915230fe1a3e22b948ceeae49e25375beaa084b7 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 02:20:33 +0200 Subject: [PATCH 39/52] Update draggable_text.js --- test/unit/draggable_text.js | 206 ++++++++++++++++++++++++++++++++++-- 1 file changed, 195 insertions(+), 11 deletions(-) diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index 359e5d9044c..f82cf633657 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -1,3 +1,14 @@ +function assertDragEventStream(name, a, b) { + QUnit.assert.equal(a.length, b.length, `${name} event stream should be same in size`); + a.forEach(({ target, dragSource, dropTarget, ..._a }, index) => { + const { target: targetB, dragSource: dragSourceB, dropTarget: dropTargetB, ..._b } = b[index]; + QUnit.assert.equal(target, targetB, `target should match ${index}`); + QUnit.assert.equal(dragSource, dragSourceB, `dragSource should match ${index}`); + QUnit.assert.equal(dropTarget, dropTargetB, `dropTarget should match ${index}`); + QUnit.assert.deepEqual(_a, _b, `event ${index} should match`); + }); +} + (fabric.getEnv().isLikelyNode ? QUnit.module.skip : QUnit.module)('draggable text', function (hooks) { let canvas; hooks.before(function () { @@ -102,6 +113,19 @@ return e; } + function createDragEvent(x = eventData.clientX, y = eventData.clientY) { + return { + ...eventData, + defaultPrevented: false, + clientX: x, + clientY: y, + }; + } + + function fireDragOver(x = eventData.clientX, y = eventData.clientY) { + canvas._onDragOver(createDragEvent(x, y)); + } + QUnit.test('click sets cursor', function (assert) { assert.equal(count, 0, 'selection:changed fired'); assert.equal(countCanvas, 0, 'text:selection:changed fired'); @@ -156,20 +180,180 @@ assert.deepEqual(eventStream.canvas, eventStream.source, 'events should match'); }); - QUnit.test.skip('drag over', function (assert) { + QUnit.test('drag over: source', function (assert) { const e = startDragging(eventData); - for (let index = 0; index < 350; index++) { - canvas._onDragOver({ - ...eventData, - defaultPrevented: false, - clientX: eventData.clientX + index * (enableRetinaScaling ? canvas._getRetinaScaling() : 1) - }); + const dragEvents = []; + let index; + for (index = 0; index < 100; index++) { + const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragOver(dragOverEvent); + dragEvents.push(dragOverEvent); + } + const dragEnd = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragEnd(dragEnd); + assertDragEventStream('source', eventStream.source, [ + { e, target: iText, type: 'dragstart' }, + { + e: dragEvents[0], + target: iText, + type: 'dragenter', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false, + pointer: new fabric.Point(30, 15), + absolutePointer: new fabric.Point(30, 15), + isClick: false, + previousTarget: undefined + }, + ...dragEvents.slice(0, 32).map(e => ({ + e, + target: iText, + type: 'dragover', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false + })), + ...dragEvents.slice(32, 93).map(e => ({ + e, + target: iText, + type: 'dragover', + subTargets: [], + dragSource: iText, + dropTarget: iText, + canDrop: true + })), + { + e: dragEvents[93], + target: iText, + type: 'dragleave', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false, + pointer: new fabric.Point(123, 15), + absolutePointer: new fabric.Point(123, 15), + isClick: false, + nextTarget: undefined + }, + { + e: dragEnd, + target: iText, + type: 'dragend', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + didDrop: false, + } + ]); + }); + + QUnit.test('drag over: target', function (assert) { + const e = startDragging(eventData); + const dragEvents = []; + let index; + for (index = 180; index < 190; index = index + 5) { + const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragOver(dragOverEvent); + dragEvents.push(dragOverEvent); + } + for (index = 0; index <= 20; index = index + 5) { + const dragOverEvent = createDragEvent(eventData.clientX + 190 * canvas.getRetinaScaling(), eventData.clientY - index * canvas.getRetinaScaling()); + canvas._onDragOver(dragOverEvent); + dragEvents.push(dragOverEvent); + } + const dragEnd = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragEnd(dragEnd); + assertDragEventStream('source in target test', eventStream.source, [ + { e, target: iText, type: 'dragstart' }, + { + e: dragEnd, + target: iText, + type: 'dragend', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + didDrop: false, + } + ]); + assertDragEventStream('target', eventStream.target, [ + { + e: dragEvents[0], + target: iText2, + type: 'dragenter', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false, + pointer: new fabric.Point(210, 15), + absolutePointer: new fabric.Point(210, 15), + isClick: false, + previousTarget: undefined + }, + ...dragEvents.slice(0, 5).map(e => ({ + e, + target: iText2, + type: 'dragover', + subTargets: [], + dragSource: iText, + dropTarget: iText2, + canDrop: true + })), + { + e: dragEvents[5], + target: iText2, + type: 'dragleave', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false, + pointer: new fabric.Point(220, 0), + absolutePointer: new fabric.Point(220, 0), + isClick: false, + nextTarget: undefined + }, + ]); + assert.deepEqual(renderEffects, [ + ...dragEvents.slice(0, 5).map(e => ({ e, source: iText, target: iText2 })), + ...dragEvents.slice(5).map(e => ({ e, source: iText, target: undefined })), + ], 'render effects'); + }); + + QUnit.test('drag over: canvas', function (assert) { + const e = startDragging(eventData); + const dragEvents = []; + let index; + for (index = 0; index < 10; index++) { + const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragOver(dragOverEvent); + dragEvents.push(dragOverEvent); } // canvas._onDrop(dragEvent); - // canvas._onDragEnd(dragEvent); - console.log(eventStream) - // assert.deepEqual(renderEffects, [{ e: eventData, source: iText, target: iText }]); - }) + const dragEnd = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragEnd(dragEnd); + assertDragEventStream('canvas', eventStream.canvas, [ + { e, target: iText, type: 'dragstart' }, + ...dragEvents.map(e => ({ + e, + target: iText, + type: 'dragover', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false + })), + { + e: dragEnd, + target: iText, + type: 'dragend', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + didDrop: false, + } + ]); + }); }); }); }); \ No newline at end of file From 4fdbd6885819a5c3ac21513820e8588f4d5bc737 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 02:45:19 +0200 Subject: [PATCH 40/52] Update draggable_text.js --- test/unit/draggable_text.js | 77 ++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index f82cf633657..e6d15e7ce76 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -54,7 +54,13 @@ function assertDragEventStream(name, a, b) { }, dataTransfer: { data: {}, + get types() { + return Object.keys(this.data); + }, dropEffect: 'none', + getData(type) { + return this.data[type]; + }, setData(type, value) { this.data[type] = value; }, @@ -90,7 +96,7 @@ function assertDragEventStream(name, a, b) { source: [], target: [], }; - ['dragstart', 'dragover', 'drag', 'dragenter', 'dragleave', 'drop', 'dragend'].forEach(type => { + ['dragstart', 'dragover', 'drag', 'dragenter', 'dragleave', 'drop', 'dragend', 'changed', 'text:changed'].forEach(type => { canvas.on(type, (ev) => { eventStream.canvas.push({ ...ev, type }); }); @@ -113,12 +119,16 @@ function assertDragEventStream(name, a, b) { return e; } - function createDragEvent(x = eventData.clientX, y = eventData.clientY) { + function createDragEvent(x = eventData.clientX, y = eventData.clientY, dataTransfer = {}) { return { ...eventData, defaultPrevented: false, clientX: x, clientY: y, + dataTransfer: { + ...eventData.dataTransfer, + ...dataTransfer + } }; } @@ -354,6 +364,69 @@ function assertDragEventStream(name, a, b) { } ]); }); + + QUnit.test('drop on drag source', function (assert) { + const e = startDragging(eventData); + const dragEvents = []; + let index; + for (index = 70; index < 80; index = index + 5) { + const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragOver(dragOverEvent); + dragEvents.push(dragOverEvent); + } + const drop = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling(), undefined, { dropEffect: 'move' }); + canvas._onDrop(drop); + canvas._onDragEnd(drop); + assertDragEventStream('drop on drag source', eventStream.source, [ + { e, target: iText, type: 'dragstart' }, + { + e: dragEvents[0], + target: iText, + type: 'dragenter', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false, + pointer: new fabric.Point(100, 15), + absolutePointer: new fabric.Point(100, 15), + isClick: false, + previousTarget: undefined + }, + ...dragEvents.slice(0, 2).map(e => ({ + e, + target: iText, + type: 'dragover', + subTargets: [], + dragSource: iText, + dropTarget: iText, + canDrop: true + })), + { + action: 'drop', + index: 4, + type: 'changed' + }, + { + e: drop, + target: iText, + type: 'drop', + subTargets: [], + dragSource: iText, + dropTarget: iText, + didDrop: true, + pointer: new fabric.Point(110, 15), + }, + { + e: drop, + target: iText, + type: 'dragend', + subTargets: [], + dragSource: iText, + dropTarget: iText, + didDrop: true, + } + ]); + }); }); }); }); \ No newline at end of file From c4886b0865229c3af222fcfe4b187deefe456254 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 02:53:39 +0200 Subject: [PATCH 41/52] Update draggable_text.js --- test/unit/draggable_text.js | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index e6d15e7ce76..69eac6a30e4 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -377,6 +377,9 @@ function assertDragEventStream(name, a, b) { const drop = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling(), undefined, { dropEffect: 'move' }); canvas._onDrop(drop); canvas._onDragEnd(drop); + assert.equal(iText.text, ' testestt', 'text after drop'); + assert.equal(iText.selectionStart, 4, 'selection after drop'); + assert.equal(iText.selectionEnd, 8, 'selection after drop'); assertDragEventStream('drop on drag source', eventStream.source, [ { e, target: iText, type: 'dragstart' }, { @@ -427,6 +430,62 @@ function assertDragEventStream(name, a, b) { } ]); }); + + QUnit.test('drop', function (assert) { + const e = startDragging(eventData); + const dragEvents = []; + let index; + for (index = 200; index < 210; index = index + 5) { + const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); + canvas._onDragOver(dragOverEvent); + dragEvents.push(dragOverEvent); + } + const drop = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling(), undefined, { dropEffect: 'move' }); + canvas._onDrop(drop); + canvas._onDragEnd(drop); + assert.equal(iText2.text, 'testestt2 test2', 'text after drop'); + assert.equal(iText2.selectionStart, 3, 'selection after drop'); + assert.equal(iText2.selectionEnd, 7, 'selection after drop'); + assertDragEventStream('drop', eventStream.target, [ + { + e: dragEvents[0], + target: iText2, + type: 'dragenter', + subTargets: [], + dragSource: iText, + dropTarget: undefined, + canDrop: false, + pointer: new fabric.Point(230, 15), + absolutePointer: new fabric.Point(230, 15), + isClick: false, + previousTarget: undefined + }, + ...dragEvents.slice(0, 2).map(e => ({ + e, + target: iText2, + type: 'dragover', + subTargets: [], + dragSource: iText, + dropTarget: iText2, + canDrop: true + })), + { + action: 'drop', + index: 3, + type: 'changed' + }, + { + e: drop, + target: iText2, + type: 'drop', + subTargets: [], + dragSource: iText, + dropTarget: iText2, + didDrop: true, + pointer: new fabric.Point(240, 15), + }, + ]); + }); }); }); }); \ No newline at end of file From 8eaae617405c9c7e08b180d539e4fc7f2b56444f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 03:28:02 +0200 Subject: [PATCH 42/52] test(): Overlapping draggable text effects --- .../overlapping_draggable_text_effects.png | Bin 0 -> 20234 bytes test/visual/text.js | 54 ++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 test/visual/golden/overlapping_draggable_text_effects.png diff --git a/test/visual/golden/overlapping_draggable_text_effects.png b/test/visual/golden/overlapping_draggable_text_effects.png new file mode 100644 index 0000000000000000000000000000000000000000..84df19917d5b95cedec9d81a38bc68adc0efec99 GIT binary patch literal 20234 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 KwK9}V*uMZ{^G*N& literal 0 HcmV?d00001 diff --git a/test/visual/text.js b/test/visual/text.js index 336b0e913c4..ca9a8955682 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -559,6 +559,60 @@ fabricClass: 'Canvas' }); + function draggableTextEffects(canvas, callback) { + const source = new fabric.IText('A draggable text\nSecond line'); + const target = new fabric.Textbox('A draggable textbox, Second line', { width: 200, left: 20, top: 20, fill: 'red' }); + canvas.add(source, target); + canvas.setActiveObject(source); + source.enterEditing(); + source.selectAll(); + canvas._onMouseDown({ + clientX: 5, + clientY: 5, + }); + canvas._onDragStart({ + clientX: 5, + clientY: 5, + preventDefault() { + + }, + stopPropagation() { + + }, + dataTransfer: { + setData() { + + }, + setDragImage(imageSource, x, y) { + + } + } + }); + canvas._onDragOver({ + clientX: 25, + clientY: 25, + preventDefault() { + + }, + stopPropagation() { + + }, + }); + canvas.getContext().drawImage(canvas.upperCanvasEl, 0, 0); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'Overlapping draggable text effects', + code: draggableTextEffects, + disabled: fabric.getEnv().isLikelyNode, + golden: 'overlapping_draggable_text_effects.png', + width: 270, + height: 120, + percentage: 0.03, + fabricClass: 'Canvas' + }); + // sinon could have spied this w/o effort and in one line class TestTextbox extends fabric.Textbox { __calledInitDimensions = 0; From e6bd3c8fe702f3cfca03d445f314bf1d64d85da0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 03:29:31 +0200 Subject: [PATCH 43/52] Update text.js --- test/visual/text.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/visual/text.js b/test/visual/text.js index ca9a8955682..9371e11b216 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -522,7 +522,7 @@ golden: 'drag_image.png', width: 120, height: 220, - percentage: 0.03, + percentage: 0.04, fabricClass: 'Canvas' }); @@ -533,7 +533,7 @@ golden: 'drag_image.png', width: 110, height: 250, - percentage: 0.03, + percentage: 0.04, fabricClass: 'Canvas' }); @@ -544,7 +544,7 @@ golden: 'drag_image_vpt.png', width: 220, height: 250, - percentage: 0.03, + percentage: 0.04, fabricClass: 'Canvas' }); @@ -555,7 +555,7 @@ golden: 'drag_image_vpt.png', width: 220, height: 250, - percentage: 0.03, + percentage: 0.04, fabricClass: 'Canvas' }); @@ -609,7 +609,7 @@ golden: 'overlapping_draggable_text_effects.png', width: 270, height: 120, - percentage: 0.03, + percentage: 0.04, fabricClass: 'Canvas' }); From 68b036c2a0d787c992fd244127bf22867e44d007 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 03:30:59 +0200 Subject: [PATCH 44/52] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d50cb817cb..e8c35c131c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ ## [next] - refactor(IText): Fixes Draggable Text for retina and viewport transform #8534 -- refactor(Animation): modernize IText cursor animation based on animation API changes (and fix minor regression) plus leftovers from #8547 [#8583](https://github.com/fabricjs/fabric.js/pull/8583) - chore(TS): remove all remaining empty declarations [#8593](https://github.com/fabricjs/fabric.js/pull/8593) - refactor(IText): modernize IText cursor animation based on animation API changes (and fix minor regression) plus leftovers from #8547 [#8583](https://github.com/fabricjs/fabric.js/pull/8583) - refactor(Canvas, IText): Handle cross instance text editing states to an EditingManager class [#8543](https://github.com/fabricjs/fabric.js/pull/8543) From fd483786eeb45df24490b4c6a266a2f3d6d08727 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 03:33:50 +0200 Subject: [PATCH 45/52] cleanup --- test/unit/draggable_text.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index 69eac6a30e4..f67c3648041 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -132,10 +132,6 @@ function assertDragEventStream(name, a, b) { }; } - function fireDragOver(x = eventData.clientX, y = eventData.clientY) { - canvas._onDragOver(createDragEvent(x, y)); - } - QUnit.test('click sets cursor', function (assert) { assert.equal(count, 0, 'selection:changed fired'); assert.equal(countCanvas, 0, 'text:selection:changed fired'); @@ -166,7 +162,6 @@ function assertDragEventStream(name, a, b) { 'text/plain': "test" }, 'should set dataTransfer'); assert.equal(e.dataTransfer.effectAllowed, 'copyMove', 'should set effectAllowed'); - // assert.equal(eventData.defaultPrevented, true, 'drag event default prevented'); assert.ok(e.dataTransfer.dragImageData.img instanceof HTMLCanvasElement, 'drag image was set'); assert.equal(e.dataTransfer.dragImageData.x, 30, 'drag image position'); assert.equal(e.dataTransfer.dragImageData.y, 15, 'drag image position'); From 4fcb2ea85d15d726b3c640191b2ecfdaeb6d18df Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 03:35:49 +0200 Subject: [PATCH 46/52] Update text.js --- test/visual/text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/visual/text.js b/test/visual/text.js index 9371e11b216..cec22636565 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -609,7 +609,7 @@ golden: 'overlapping_draggable_text_effects.png', width: 270, height: 120, - percentage: 0.04, + percentage: 0.05, fabricClass: 'Canvas' }); From 565a9a8d3d64a44f1ee115002c68f0b756f7fff7 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 13 Jan 2023 08:40:14 +0200 Subject: [PATCH 47/52] renderEffects --- test/unit/draggable_text.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index f67c3648041..2224db0fda9 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -252,6 +252,11 @@ function assertDragEventStream(name, a, b) { didDrop: false, } ]); + assert.deepEqual(renderEffects, [ + ...dragEvents.slice(0, 32).map(e => ({ e, source: iText, target: undefined })), + ...dragEvents.slice(32, 93).map(e => ({ e, source: iText, target: iText })), + ...dragEvents.slice(93).map(e => ({ e, source: iText, target: undefined })), + ], 'render effects'); }); QUnit.test('drag over: target', function (assert) { From fa0a91dc8165b05158ad7956712795655baf4169 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 14 Jan 2023 09:03:01 +0200 Subject: [PATCH 48/52] Update canvas_events.ts --- src/canvas/canvas_events.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index e13cdf03286..e3ef5096835 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -780,7 +780,10 @@ export class Canvas extends SelectableCanvas { _onMouseMove(e: TPointerEvent) { const activeObject = this.getActiveObject(); !this.allowTouchScrolling && - (!activeObject || !activeObject.__isDragging) && + (!activeObject || + // active object will flag itself on mousedown/mousedown:before + // we need to prevent default so the window starts the drag event sequence and stop mousemove + !activeObject.__isDragging) && e.preventDefault && e.preventDefault(); this.__onMouseMove(e); From f4fee329263c0f1b0e3e813f716723910942b376 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 14 Jan 2023 09:36:25 +0200 Subject: [PATCH 49/52] fix(): focus hiddentextarea in edge cases --- src/mixins/itext_behavior.mixin.ts | 5 ++++- test/unit/draggable_text.js | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index bc70316b347..2cc5161f825 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -632,15 +632,17 @@ export abstract class ITextBehaviorMixin< dragEndHandler({ e }: DragEventData) { if (this.__isDragging && this.__dragStartFired) { // once the drop event finishes we check if we need to change the drag source - // if the drag source received the drop we bail out + // if the drag source received the drop we bail out since the drop handler has already handled logic if (this.__dragStartSelection) { const selectionStart = this.__dragStartSelection.selectionStart; const selectionEnd = this.__dragStartSelection.selectionEnd; const dropEffect = e.dataTransfer.dropEffect; if (dropEffect === 'none') { + // pointer is back over selection this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; this._updateTextarea(); + this.hiddenTextarea.focus(); } else { this.clearContextTop(); if (dropEffect === 'move') { @@ -729,6 +731,7 @@ export abstract class ITextBehaviorMixin< ); this.hiddenTextarea && (this.hiddenTextarea.value = this.text); this._updateTextarea(); + this.hiddenTextarea.focus(); this.fire('changed', { index: insertAt + selectionStartOffset, action: 'drop', diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index 2224db0fda9..4be8b68b971 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -151,6 +151,13 @@ function assertDragEventStream(name, a, b) { assert.equal(countCanvas, 1, 'text:selection:changed fired'); }); + QUnit.test('drag end over selection focuses hiddenTextarea', function (assert) { + startDragging(eventData); + iText.hiddenTextarea.blur(); + canvas._onDragEnd(createDragEvent()); + assert.equal(fabric.getDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); + }); + QUnit.test('drag start', function (assert) { const e = startDragging(eventData); const charStyle = { "stroke": null, "strokeWidth": 1, "fill": "rgb(0,0,0)", "fontFamily": "Times New Roman", "fontSize": 40, "fontWeight": "normal", "fontStyle": "normal", "underline": false, "overline": false, "linethrough": false, "deltaY": 0, "textBackgroundColor": "" }; @@ -257,6 +264,7 @@ function assertDragEventStream(name, a, b) { ...dragEvents.slice(32, 93).map(e => ({ e, source: iText, target: iText })), ...dragEvents.slice(93).map(e => ({ e, source: iText, target: undefined })), ], 'render effects'); + assert.equal(fabric.getDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); }); QUnit.test('drag over: target', function (assert) { @@ -328,6 +336,7 @@ function assertDragEventStream(name, a, b) { ...dragEvents.slice(0, 5).map(e => ({ e, source: iText, target: iText2 })), ...dragEvents.slice(5).map(e => ({ e, source: iText, target: undefined })), ], 'render effects'); + assert.equal(fabric.getDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); }); QUnit.test('drag over: canvas', function (assert) { @@ -363,6 +372,7 @@ function assertDragEventStream(name, a, b) { didDrop: false, } ]); + assert.equal(fabric.getDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); }); QUnit.test('drop on drag source', function (assert) { @@ -429,6 +439,7 @@ function assertDragEventStream(name, a, b) { didDrop: true, } ]); + assert.equal(fabric.getDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); }); QUnit.test('drop', function (assert) { @@ -485,6 +496,7 @@ function assertDragEventStream(name, a, b) { pointer: new fabric.Point(240, 15), }, ]); + assert.equal(fabric.getDocument().activeElement, iText2.hiddenTextarea, 'should have focused hiddenTextarea'); }); }); }); From de198d401db11edd3978899952d357af4e609c2c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 14 Jan 2023 09:40:40 +0200 Subject: [PATCH 50/52] correct comment --- src/canvas/canvas_events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index e3ef5096835..4335d7c3600 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -781,8 +781,8 @@ export class Canvas extends SelectableCanvas { const activeObject = this.getActiveObject(); !this.allowTouchScrolling && (!activeObject || - // active object will flag itself on mousedown/mousedown:before - // we need to prevent default so the window starts the drag event sequence and stop mousemove + // a drag event sequence is started by the active object flagging itself on mousedown / mousedown:before + // we must not prevent the event's default behavior in order for the window to start the drag event sequence !activeObject.__isDragging) && e.preventDefault && e.preventDefault(); From 3dd25c254133453ec4d579e64fc66c05f3c9c204 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 16 Jan 2023 00:51:24 +0100 Subject: [PATCH 51/52] added coment and extract dropTarget variable --- src/canvas/canvas_events.ts | 9 +++++---- src/mixins/itext_click_behavior.mixin.ts | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index 9adf710305e..c438caf59da 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -337,12 +337,13 @@ export class Canvas extends SelectableCanvas { ) { let dirty = false; // clear top context + const dropTarget = this._dropTarget; if ( - this._dropTarget && - this._dropTarget !== source && - this._dropTarget !== target + dropTarget && + dropTarget !== source && + dropTarget !== target ) { - this._dropTarget.clearContextTop(); + dropTarget.clearContextTop(); dirty = true; } source?.clearContextTop(); diff --git a/src/mixins/itext_click_behavior.mixin.ts b/src/mixins/itext_click_behavior.mixin.ts index 32669fb30fa..df6916949a6 100644 --- a/src/mixins/itext_click_behavior.mixin.ts +++ b/src/mixins/itext_click_behavior.mixin.ts @@ -195,6 +195,9 @@ export abstract class ITextClickBehaviorMixin< return; } + // mousedown is going to early return if isDragging is true. + // this is here to recover the setCursorByClick in case the + // isDragging is a false positive. shouldSetCursor && this.setCursorByClick(options.e); if (this.__lastSelected && !this.__corner) { From 7ecb5ea9038c7ff4bc1391c349839741c8d5b7a4 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 16 Jan 2023 09:29:25 +0100 Subject: [PATCH 52/52] prettier --- src/canvas/canvas_events.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index c438caf59da..e71ed3d4e4e 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -338,11 +338,7 @@ export class Canvas extends SelectableCanvas { let dirty = false; // clear top context const dropTarget = this._dropTarget; - if ( - dropTarget && - dropTarget !== source && - dropTarget !== target - ) { + if (dropTarget && dropTarget !== source && dropTarget !== target) { dropTarget.clearContextTop(); dirty = true; }