New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

fabric

Package Overview
Dependencies
Maintainers
2
Versions
309
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fabric - npm Package Compare versions

Comparing version 6.0.0-beta17 to 6.0.0-beta18

dist/fabric.d.ts.map

16

dist/src/canvas/Canvas.d.ts

@@ -212,15 +212,3 @@ import type { CanvasEvents, DragEventData, ObjectEvents, TPointerEvent, TPointerEventNames, Transform } from '../EventTypeDefs';

/**
* End the current transform.
* You don't usually need to call this method unless you are interrupting a user initiated transform
* because of some other event ( a press of key combination, or something that block the user UX )
* @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event
*/
endCurrentTransform(e: TPointerEvent): void;
/**
* @private
* @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event
*/
_finalizeCurrentTransform(e: TPointerEvent): void;
/**
* @private
* @param {Event} e Event object fired on mousedown

@@ -260,6 +248,2 @@ */

/**
* @private
*/
_beforeTransform(e: TPointerEvent): void;
/**
* Method that defines the actions when mouse is hovering the canvas.

@@ -266,0 +250,0 @@ * The currentTransform parameter will define whether the user is rotating/scaling/translating

import type { ModifierKey, TOptionalModifierKey } from '../EventTypeDefs';
import type { ActiveSelection } from '../shapes/ActiveSelection';
import type { TOptions } from '../typedefs';

@@ -228,6 +227,4 @@ import type { StaticCanvasOptions } from './StaticCanvasOptions';

}
export type TCanvasOptions = TOptions<CanvasOptions & {
activeSelection: ActiveSelection;
}>;
export type TCanvasOptions = TOptions<CanvasOptions>;
export declare const canvasDefaults: TOptions<CanvasOptions>;
//# sourceMappingURL=CanvasOptions.d.ts.map

28

dist/src/canvas/SelectableCanvas.d.ts

@@ -8,5 +8,4 @@ import { Point } from '../Point';

import type { BaseBrush } from '../brushes/BaseBrush';
import { ActiveSelection } from '../shapes/ActiveSelection';
import { CanvasDOMManager } from './DOMManagers/CanvasDOMManager';
import type { CanvasOptions, TCanvasOptions } from './CanvasOptions';
import type { CanvasOptions } from './CanvasOptions';
/**

@@ -229,4 +228,2 @@ * Canvas class

_activeObject?: FabricObject;
protected _activeSelection: ActiveSelection;
constructor(el?: string | HTMLCanvasElement, { activeSelection, ...options }?: TCanvasOptions);
protected initElements(el?: string | HTMLCanvasElement): void;

@@ -321,3 +318,4 @@ /**

* @param {Event} e Event object
* @param {FaricObject} target
* @param {FabricObject} target
* @param {boolean} [alreadySelected] pass true to setup the active control
*/

@@ -444,6 +442,2 @@ _setupCurrentTransform(e: TPointerEvent, target: FabricObject, alreadySelected: boolean): void;

/**
* Returns instance's active selection
*/
getActiveSelection(): ActiveSelection;
/**
* Returns an array with the current selected objects

@@ -499,2 +493,14 @@ * @return {FabricObject[]} active objects array

/**
* End the current transform.
* You don't usually need to call this method unless you are interrupting a user initiated transform
* because of some other event ( a press of key combination, or something that block the user UX )
* @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event
*/
endCurrentTransform(e?: TPointerEvent): void;
/**
* @private
* @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event
*/
_finalizeCurrentTransform(e?: TPointerEvent): void;
/**
* Sets viewport transformation of this canvas instance

@@ -520,3 +526,3 @@ * @param {Array} vpt a Canvas 2D API transform matrix

*/
_toObject(instance: FabricObject, methodName: 'toObject' | 'toDatalessObject', propertiesToInclude: string[]): Record<string, any>;
protected _toObject(instance: FabricObject, methodName: 'toObject' | 'toDatalessObject', propertiesToInclude: string[]): Record<string, any>;
/**

@@ -528,3 +534,3 @@ * Realizes an object's group transformation on it

*/
_realizeGroupTransformOnObject(instance: FabricObject): Partial<typeof instance>;
private _realizeGroupTransformOnObject;
/**

@@ -531,0 +537,0 @@ * @private

@@ -37,3 +37,5 @@ import type { CanvasEvents, StaticCanvasEvents } from '../EventTypeDefs';

remove(...objects: FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>[]): FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>[];
forEachObject(callback: (object: FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>, index: number, array: FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>[]) => any): void;
forEachObject(callback: (object: FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>, index: number, array: FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>[]) => any): void; /**
* The viewport bounding box in scene plane coordinates, see {@link calcViewportBoundaries}
*/
getObjects(...types: string[]): FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>[];

@@ -138,6 +140,3 @@ item(index: number): FabricObject<Partial<import("../..").FabricObjectProps>, import("../..").SerializedObjectProps, import("../EventTypeDefs").ObjectEvents>;

* @private
*/
_isRetinaScaling(): boolean;
/**
* @private
* @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html
* @return {Number} retinaScaling if applied, otherwise 1;

@@ -405,3 +404,3 @@ */

*/
_toObject(instance: FabricObject, methodName: TValidToObjectMethod, propertiesToInclude?: string[]): any;
protected _toObject(instance: FabricObject, methodName: TValidToObjectMethod, propertiesToInclude?: string[]): any;
/**

@@ -408,0 +407,0 @@ * @private

@@ -7,3 +7,3 @@ export declare const JSON = "json";

constructor();
getClass(classType: string): any;
getClass<T>(classType: string): T;
setClass(classConstructor: any, classType?: string): void;

@@ -10,0 +10,0 @@ getSVGClass(SVGTagName: string): any;

@@ -13,3 +13,3 @@ import type { TPointerEvent, Transform, TransformAction, BasicTransformEvent } from '../EventTypeDefs';

*/
export declare const getActionFromCorner: (alreadySelected: boolean, corner: string, e: TPointerEvent, target: FabricObject) => string;
export declare const getActionFromCorner: (alreadySelected: boolean, corner: string | undefined, e: TPointerEvent, target: FabricObject) => string;
/**

@@ -16,0 +16,0 @@ * Checks if transform is centered

/**
* This file is consumed by fabric.
* The `./node` and `./browser` files define the env variable that is used by this module.
* The `./node` module sets the env at import time.
* The `./browser` module is defined to be the default env and doesn't set the env at all.

@@ -25,5 +24,12 @@ * This is done in order to support isomorphic usage for browser and node applications

export declare const setEnv: (value: TFabricEnv) => void;
/**
* In order to support SSR we **MUST** access the browser env only after the window has loaded
*/
export declare const getEnv: () => TFabricEnv;
export declare const getFabricDocument: () => Document;
export declare const getFabricWindow: () => (Window & typeof globalThis) | DOMWindow;
/**
* @returns the config value if defined, fallbacks to the environment value
*/
export declare const getDevicePixelRatio: () => number;
//# sourceMappingURL=index.d.ts.map

@@ -9,5 +9,5 @@ import type { GLProbe } from '../filters/GLProbes/GLProbe';

export type TFabricEnv = {
document: Document;
window: (Window & typeof globalThis) | DOMWindow;
isTouchSupported: boolean;
readonly document: Document;
readonly window: (Window & typeof globalThis) | DOMWindow;
readonly isTouchSupported: boolean;
WebGLProbe: GLProbe;

@@ -14,0 +14,0 @@ dispose(element: Element): void;

@@ -33,3 +33,3 @@ import type { Control } from './controls/Control';

target: FabricObject;
action: string;
action?: string;
actionHandler?: TransformActionHandler;

@@ -58,3 +58,2 @@ corner: string;

};
reset?: boolean;
actionPerformed: boolean;

@@ -73,6 +72,7 @@ };

export type TModificationEvents = 'moving' | 'scaling' | 'rotating' | 'skewing' | 'resizing' | 'modifyPoly';
export interface ModifiedEvent<E extends Event = TPointerEvent> extends TEvent<E> {
export interface ModifiedEvent<E extends Event = TPointerEvent> {
e?: E;
transform: Transform;
target: FabricObject;
action: string;
action?: string;
}

@@ -79,0 +79,0 @@ type ModificationEventsSpec<Prefix extends string = '', Modification = BasicTransformEvent, Modified = ModifiedEvent | never> = Record<`${Prefix}${TModificationEvents}`, Modification> & Record<`${Prefix}modified`, Modified>;

@@ -5,4 +5,4 @@ export * as filters from './filters';

export { WebGLFilterBackend } from './WebGLFilterBackend';
export { isWebGLPipelineState } from './utils';
export { isWebGLPipelineState, isPutImageFaster } from './utils';
export * from './typedefs';
//# sourceMappingURL=index.d.ts.map
import type { TWebGLPipelineState, T2DPipelineState } from './typedefs';
export declare const isWebGLPipelineState: (options: TWebGLPipelineState | T2DPipelineState) => options is TWebGLPipelineState;
/**
* Pick a method to copy data from GL context to 2d canvas. In some browsers using
* drawImage should be faster, but is also bugged for a small combination of old hardware
* and drivers.
* putImageData is faster than drawImage for that specific operation.
*/
export declare const isPutImageFaster: (width: number, height: number) => boolean;
//# sourceMappingURL=utils.d.ts.map

@@ -47,9 +47,2 @@ import type { TWebGLPipelineState, TProgramCache, TTextureCache, TPipelineResources } from './typedefs';

/**
* Pick a method to copy data from GL context to 2d canvas. In some browsers using
* drawImage should be faster, but is also bugged for a small combination of old hardware
* and drivers.
* putImageData is faster than drawImage for that specific operation.
*/
chooseFastestCopyGLTo2DMethod(width: number, height: number): void;
/**
* Create a canvas element and associated WebGL context and attaches them as

@@ -122,2 +115,11 @@ * class properties to the GLFilterBackend class.

/**
* Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData
* API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra).
*
* @param {WebGLRenderingContext} sourceContext The WebGL context to copy from.
* @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to.
* @param {Object} pipelineState The 2D target canvas to copy on to.
*/
copyGLTo2DPutImageData(this: Required<WebGLFilterBackend>, gl: WebGLRenderingContext, pipelineState: TWebGLPipelineState): void;
/**
* Attempt to extract GPU information strings from a WebGL context.

@@ -124,0 +126,0 @@ *

@@ -24,6 +24,2 @@ import { Point } from '../../Point';

/**
* called from the `onAfterLayout` hook
*/
shouldResetTransform(context: StrictLayoutContext): boolean;
/**
* Override this method to customize layout.

@@ -30,0 +26,0 @@ */

@@ -11,3 +11,2 @@ import type { ControlRenderingStyleOverride } from '../controls/controlRendering';

* Used by Canvas to manage selection.
* Canvas accepts an `activeSelection` option allowing overriding and customization.
*

@@ -19,7 +18,11 @@ * @example

*
* const canvas = new Canvas(el, {
* activeSelection: new MyActiveSelection()
* })
* // override the default `ActiveSelection` class
* classRegistry.setClass(MyActiveSelection)
*/
export declare class ActiveSelection extends Group {
static type: string;
static ownDefaults: Record<string, any>;
static getDefaults(): {
[x: string]: any;
};
/**

@@ -33,3 +36,2 @@ * controls how selected objects are added during a multiselection event

multiSelectionStacking: MultiSelectionStacking;
static type: string;
/**

@@ -71,5 +73,3 @@ * @private

/**
* If returns true, deselection is cancelled.
* @since 2.0.0
* @return {Boolean} [cancel]
* @override remove all objects
*/

@@ -76,0 +76,0 @@ onDeselect(): boolean;

@@ -290,3 +290,3 @@ import type { BaseFilter } from '../filters/BaseFilter';

*/
static fromObject<T extends TOptions<SerializedImageProps>>({ filters: f, resizeFilter: rf, src, crossOrigin, type, ...object }: T, options?: Abortable): Promise<FabricImage<Omit<T, "crossOrigin" | "type" | "filters" | "resizeFilter" | "src"> & {
static fromObject<T extends TOptions<SerializedImageProps>>({ filters: f, resizeFilter: rf, src, crossOrigin, type, ...object }: T, options?: Abortable): Promise<FabricImage<Omit<T, "type" | "crossOrigin" | "filters" | "resizeFilter" | "src"> & {
src: string | undefined;

@@ -293,0 +293,0 @@ filters: BaseFilter[];

@@ -103,3 +103,7 @@ import { Point } from '../../Point';

_updateCacheCanvas(): boolean;
getActiveControl(): string | undefined;
getActiveControl(): {
key: string;
control: Control;
coord: TOCoord;
} | undefined;
/**

@@ -106,0 +110,0 @@ * Determines which corner is under the mouse cursor, represented by `pointer`.

@@ -81,7 +81,11 @@ import type { ObjectEvents } from '../../EventTypeDefs';

/**
* get the reference, not a clone, of the style object for a given character,
* if not style is set for a pre det
* Get a reference, not a clone, to the style object for a given character,
* if no style is set for a line or char, return a new empty object.
* This is tricky and confusing because when you get an empty object you can't
* determine if it is a reference or a new one.
* @TODO this should always return a reference or always a clone or undefined when necessary.
* @protected
* @param {Number} lineIndex
* @param {Number} charIndex
* @return {Object} style object a REFERENCE to the existing one or a new empty object
* @return {TextStyleDeclaration} a style object reference to the existing one or a new empty object when undefined
*/

@@ -88,0 +92,0 @@ _getStyleDeclaration(lineIndex: number, charIndex: number): TextStyleDeclaration;

import type { TSVGReviver } from '../../typedefs';
import { FabricObjectSVGExportMixin } from '../Object/FabricObjectSVGExportMixin';
import type { TextStyleDeclaration } from './StyledText';
import { type TextStyleDeclaration } from './StyledText';
import type { FabricText } from './Text';

@@ -5,0 +5,0 @@ export declare class TextSVGExportMixin extends FabricObjectSVGExportMixin {

@@ -104,5 +104,6 @@ import type { TClassProperties, TOptions } from '../typedefs';

/**
* @protected
* @param {Number} lineIndex
* @param {Number} charIndex
* @private
* @return {TextStyleDeclaration} a style object reference to the existing one or a new empty object when undefined
*/

@@ -122,3 +123,3 @@ _getStyleDeclaration(lineIndex: number, charIndex: number): TextStyleDeclaration;

*/
_deleteStyleDeclaration(lineIndex: number, charIndex: number): void;
protected _deleteStyleDeclaration(lineIndex: number, charIndex: number): void;
/**

@@ -125,0 +126,0 @@ * probably broken need a fix

@@ -6,2 +6,3 @@ import type { FabricObject } from '../shapes/Object/Object';

import type { Path } from '../shapes/Path';
import type { ActiveSelection } from '../shapes/ActiveSelection';
export declare const isFiller: (filler: TFiller | string | null) => filler is TFiller;

@@ -12,2 +13,3 @@ export declare const isSerializableFiller: (filler: TFiller | string | null) => filler is TFiller;

export declare const isPath: (fabricObject?: FabricObject) => fabricObject is Path<Partial<import("../shapes/Path").PathProps>, import("../shapes/Path").SerializedPathProps, import("../EventTypeDefs").ObjectEvents>;
export declare const isActiveSelection: (fabricObject?: FabricObject) => fabricObject is ActiveSelection;
//# sourceMappingURL=typeAssertions.d.ts.map

@@ -1,4 +0,10 @@

// first we set the env variable by importing the node env file
import { getNodeCanvas } from './src/env/node';
// First we set the env variable
import { setEnv } from './src/env';
import { getEnv, getNodeCanvas } from './src/env/node';
setEnv(getEnv());
// After the env is set we can export everything and expose specific node functionality
import type { JpegConfig, PngConfig } from 'canvas';

@@ -5,0 +11,0 @@ import {

@@ -5,3 +5,3 @@ {

"homepage": "http://fabricjs.com/",
"version": "6.0.0-beta17",
"version": "6.0.0-beta18",
"author": "Juriy Zaytsev <kangax@gmail.com>",

@@ -121,3 +121,3 @@ "contributors": [

"qunit": "^2.17.2",
"rollup": "^3.21.7",
"rollup": "^4.9.5",
"semver": "^7.3.8",

@@ -124,0 +124,0 @@ "source-map-support": "^0.5.21",

/* eslint-disable no-restricted-globals */
import '../../../jest.extend';
import type { TPointerEvent } from '../../EventTypeDefs';
import { Point } from '../../Point';
import { ActiveSelection } from '../../shapes/ActiveSelection';
import { Circle } from '../../shapes/Circle';
import { Group } from '../../shapes/Group';
import { IText } from '../../shapes/IText/IText';
import { FabricObject } from '../../shapes/Object/FabricObject';
import { Rect } from '../../shapes/Rect';
import { Triangle } from '../../shapes/Triangle';
import type { TMat2D } from '../../typedefs';

@@ -13,2 +16,8 @@ import { Canvas } from '../Canvas';

const registerTestObjects = (objects: Record<string, FabricObject>) => {
Object.entries(objects).forEach(([key, object]) => {
jest.spyOn(object, 'toJSON').mockReturnValue(key);
});
};
describe('Canvas event data', () => {

@@ -135,36 +144,753 @@ let canvas: Canvas;

it('A selected subtarget should not fire an event twice', () => {
const target = new FabricObject();
const group = new Group([target], {
subTargetCheck: true,
interactive: true,
describe('Event targets', () => {
it('A selected subtarget should not fire an event twice', () => {
const target = new FabricObject();
const group = new Group([target], {
subTargetCheck: true,
interactive: true,
});
const canvas = new Canvas();
canvas.add(group);
const targetSpy = jest.fn();
target.on('mousedown', targetSpy);
jest.spyOn(canvas, '_checkTarget').mockReturnValue(true);
canvas.getSelectionElement().dispatchEvent(
new MouseEvent('mousedown', {
clientX: 50,
clientY: 50,
})
);
expect(targetSpy).toHaveBeenCalledTimes(1);
});
const canvas = new Canvas();
canvas.add(group);
const targetSpy = jest.fn();
target.on('mousedown', targetSpy);
jest.spyOn(canvas, '_checkTarget').mockReturnValue(true);
canvas.__onMouseDown({
target: canvas.getSelectionElement(),
clientX: 0,
clientY: 0,
} as unknown as TPointerEvent);
expect(targetSpy).toHaveBeenCalledTimes(1);
});
it('should fire mouse over/out events on target', () => {
const target = new FabricObject({ width: 10, height: 10 });
const canvas = new Canvas();
canvas.add(target);
test('mouseover and mouseout with subTargetCheck', () => {
const rect1 = new FabricObject({
width: 5,
height: 5,
left: 5,
top: 0,
strokeWidth: 0,
});
const rect2 = new FabricObject({
width: 5,
height: 5,
left: 5,
top: 5,
strokeWidth: 0,
});
const rect3 = new FabricObject({
width: 5,
height: 5,
left: 0,
top: 5,
strokeWidth: 0,
});
const rect4 = new FabricObject({
width: 5,
height: 5,
left: 0,
top: 0,
strokeWidth: 0,
});
const rect5 = new FabricObject({
width: 5,
height: 5,
left: 2.5,
top: 2.5,
strokeWidth: 0,
});
const group1 = new Group([rect1, rect2], {
subTargetCheck: true,
});
const group2 = new Group([rect3, rect4], {
subTargetCheck: true,
});
// a group with 2 groups, with 2 rects each, one group left one group right
// each with 2 rects vertically aligned
const group = new Group([group1, group2], {
subTargetCheck: true,
});
jest.spyOn(target, 'toJSON').mockReturnValue('target');
const enter = jest.fn();
const exit = jest.fn();
const targetSpy = jest.spyOn(target, 'fire');
const canvasSpy = jest.spyOn(canvas, 'fire');
const enter = new MouseEvent('mousemove', { clientX: 5, clientY: 5 });
const exit = new MouseEvent('mousemove', { clientX: 20, clientY: 20 });
canvas._onMouseMove(enter);
canvas._onMouseMove(exit);
expect(targetSpy.mock.calls).toMatchSnapshot();
expect(canvasSpy.mock.calls).toMatchSnapshot();
const getTargetsFromEventStream = (mock: jest.Mock) =>
mock.mock.calls.map((args) => args[0].target);
registerTestObjects({
rect1,
rect2,
rect3,
rect4,
rect5,
group1,
group2,
group,
});
Object.values({
rect1,
rect2,
rect3,
rect4,
rect5,
group1,
group2,
group,
}).forEach((object) => {
object.on('mouseover', enter);
object.on('mouseout', exit);
});
const canvas = new Canvas();
canvas.add(group, rect5);
const fire = (x: number, y: number) => {
enter.mockClear();
exit.mockClear();
canvas
.getSelectionElement()
.dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y }));
};
fire(1, 1);
expect(getTargetsFromEventStream(enter)).toEqual([group, rect4, group2]);
expect(getTargetsFromEventStream(exit)).toEqual([]);
fire(5, 5);
expect(getTargetsFromEventStream(enter)).toEqual([rect5]);
expect(getTargetsFromEventStream(exit)).toEqual([group, rect4, group2]);
fire(9, 9);
expect(getTargetsFromEventStream(enter)).toEqual([group, rect2, group1]);
expect(getTargetsFromEventStream(exit)).toEqual([rect5]);
fire(9, 1);
expect(getTargetsFromEventStream(enter)).toEqual([rect1]);
expect(getTargetsFromEventStream(exit)).toEqual([rect2]);
});
describe('findTarget', () => {
const mockEvent = ({
canvas,
...init
}: MouseEventInit & { canvas: Canvas }) => {
const e = new MouseEvent('mousedown', {
...init,
});
jest
.spyOn(e, 'target', 'get')
.mockReturnValue(canvas.getSelectionElement());
return e;
};
const findTarget = (canvas: Canvas, ev?: MouseEventInit) => {
const target = canvas.findTarget(
mockEvent({ canvas, clientX: 0, clientY: 0, ...ev })
);
const targets = canvas.targets;
canvas.targets = [];
return { target, targets };
};
test.skip.each([true, false])(
'findTargetsTraversal: search all is %s',
(searchAll) => {
const subTarget1 = new FabricObject();
const target1 = new Group([subTarget1], {
subTargetCheck: true,
interactive: true,
});
const subTarget2 = new FabricObject();
const target2 = new Group([subTarget2], {
subTargetCheck: true,
});
const parent = new Group([target1, target2], {
subTargetCheck: true,
interactive: true,
});
registerTestObjects({
subTarget1,
target1,
subTarget2,
target2,
parent,
});
const canvas = new Canvas();
canvas.add(parent);
jest.spyOn(canvas, '_checkTarget').mockReturnValue(true);
const found = canvas['findTargetsTraversal']([parent], new Point(), {
searchStrategy: searchAll ? 'search-all' : 'first-hit',
});
expect(found).toEqual(
searchAll
? [subTarget2, target2, subTarget1, target1, parent]
: [subTarget2, target2, parent]
);
}
);
test.failing('searchPossibleTargets', () => {
const subTarget = new FabricObject();
const target = new Group([subTarget], {
subTargetCheck: true,
});
const parent = new Group([target], {
subTargetCheck: true,
interactive: true,
});
registerTestObjects({ subTarget, target, parent });
const canvas = new Canvas();
canvas.add(parent);
jest.spyOn(canvas, '_checkTarget').mockReturnValue(true);
const found = canvas.searchPossibleTargets([parent], new Point());
expect(found).toBe(target);
expect(canvas.targets).toEqual([subTarget, target, parent]);
});
test('searchPossibleTargets with selection', () => {
const subTarget = new FabricObject();
const target = new Group([subTarget], {
subTargetCheck: true,
});
const other = new FabricObject();
const activeSelection = new ActiveSelection();
registerTestObjects({ subTarget, target, other, activeSelection });
const canvas = new Canvas(undefined, { activeSelection });
canvas.add(other, target);
activeSelection.add(target, other);
canvas.setActiveObject(activeSelection);
jest.spyOn(canvas, '_checkTarget').mockReturnValue(true);
const found = canvas.searchPossibleTargets(
[activeSelection],
new Point()
);
expect(found).toBe(activeSelection);
expect(canvas.targets).toEqual([]);
});
test('findTarget clears prev targets', () => {
const canvas = new Canvas();
canvas.targets = [new FabricObject()];
expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({
target: undefined,
targets: [],
});
});
test('findTarget preserveObjectStacking false', () => {
const rect = new FabricObject({
left: 0,
top: 0,
width: 10,
height: 10,
controls: {},
});
const rectOver = new FabricObject({
left: 0,
top: 0,
width: 10,
height: 10,
controls: {},
});
registerTestObjects({ rect, rectOver });
const canvas = new Canvas(undefined, { preserveObjectStacking: false });
canvas.add(rect, rectOver);
canvas.setActiveObject(rect);
expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({
target: rect,
targets: [],
});
});
test('findTarget preserveObjectStacking true', () => {
const rect = new FabricObject({ left: 0, top: 0, width: 30, height: 30 });
const rectOver = new FabricObject({
left: 0,
top: 0,
width: 30,
height: 30,
});
registerTestObjects({ rect, rectOver });
const canvas = new Canvas(undefined, { preserveObjectStacking: true });
canvas.add(rect, rectOver);
const e = {
clientX: 15,
clientY: 15,
shiftKey: true,
};
const e2 = { clientX: 4, clientY: 4 };
expect(findTarget(canvas, e)).toEqual(
{ target: rectOver, targets: [] }
// 'Should return the rectOver, rect is not considered'
);
canvas.setActiveObject(rect);
expect(findTarget(canvas, e)).toEqual(
{ target: rectOver, targets: [] }
// 'Should still return rectOver because is above active object'
);
expect(findTarget(canvas, e2)).toEqual(
{ target: rect, targets: [] }
// 'Should rect because a corner of the activeObject has been hit'
);
canvas.altSelectionKey = 'shiftKey';
expect(findTarget(canvas, e)).toEqual(
{ target: rect, targets: [] }
// 'Should rect because active and altSelectionKey is pressed'
);
});
test('findTarget with subTargetCheck', () => {
const canvas = new Canvas();
const rect = new FabricObject({ left: 0, top: 0, width: 10, height: 10 });
const rect2 = new FabricObject({
left: 30,
top: 30,
width: 10,
height: 10,
});
const group = new Group([rect, rect2]);
registerTestObjects({ rect, rect2, group });
canvas.add(group);
expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({
target: group,
targets: [],
});
expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({
target: group,
targets: [],
});
group.subTargetCheck = true;
group.setCoords();
expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({
target: group,
targets: [rect],
});
expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({
target: group,
targets: [],
});
expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({
target: group,
targets: [rect2],
});
});
test('findTarget with subTargetCheck and canvas zoom', () => {
const nested1 = new FabricObject({
width: 100,
height: 100,
fill: 'yellow',
});
const nested2 = new FabricObject({
width: 100,
height: 100,
left: 100,
top: 100,
fill: 'purple',
});
const nestedGroup = new Group([nested1, nested2], {
scaleX: 0.5,
scaleY: 0.5,
top: 100,
left: 0,
subTargetCheck: true,
});
const rect1 = new FabricObject({
width: 100,
height: 100,
fill: 'red',
});
const rect2 = new FabricObject({
width: 100,
height: 100,
left: 100,
top: 100,
fill: 'blue',
});
const group = new Group([rect1, rect2, nestedGroup], {
top: -150,
left: -50,
subTargetCheck: true,
});
registerTestObjects({
rect1,
rect2,
nested1,
nested2,
nestedGroup,
group,
});
const canvas = new Canvas(undefined, {
viewportTransform: [0.1, 0, 0, 0.1, 100, 200],
});
canvas.add(group);
expect(findTarget(canvas, { clientX: 96, clientY: 186 })).toEqual({
target: group,
targets: [rect1],
});
expect(findTarget(canvas, { clientX: 98, clientY: 188 })).toEqual({
target: group,
targets: [rect1],
});
expect(findTarget(canvas, { clientX: 100, clientY: 190 })).toEqual({
target: group,
targets: [rect1],
});
expect(findTarget(canvas, { clientX: 102, clientY: 192 })).toEqual({
target: group,
targets: [rect1],
});
expect(findTarget(canvas, { clientX: 104, clientY: 194 })).toEqual({
target: group,
targets: [rect1],
});
expect(findTarget(canvas, { clientX: 106, clientY: 196 })).toEqual({
target: group,
targets: [rect2],
});
});
test.each([true, false])(
'findTarget on activeObject with subTargetCheck and preserveObjectStacking %s',
(preserveObjectStacking) => {
const rect = new FabricObject({
left: 0,
top: 0,
width: 10,
height: 10,
});
const rect2 = new FabricObject({
left: 30,
top: 30,
width: 10,
height: 10,
});
const group = new Group([rect, rect2], { subTargetCheck: true });
registerTestObjects({ rect, rect2, group });
const canvas = new Canvas(undefined, { preserveObjectStacking });
canvas.add(group);
canvas.setActiveObject(group);
expect(findTarget(canvas, { clientX: 9, clientY: 9 })).toEqual({
target: group,
targets: [rect],
});
}
);
test('findTarget with perPixelTargetFind', () => {
const triangle = new Triangle({ width: 30, height: 30 });
registerTestObjects({ triangle });
const canvas = new Canvas();
canvas.add(triangle);
expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({
target: triangle,
targets: [],
});
canvas.perPixelTargetFind = true;
expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({
target: undefined,
targets: [],
});
expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({
target: triangle,
targets: [],
});
});
describe('findTarget with perPixelTargetFind in nested group', () => {
const prepareTest = () => {
const deepTriangle = new Triangle({
left: 0,
top: 0,
width: 30,
height: 30,
fill: 'yellow',
});
const triangle2 = new Triangle({
left: 100,
top: 120,
width: 30,
height: 30,
angle: 100,
fill: 'pink',
});
const deepCircle = new Circle({
radius: 30,
top: 0,
left: 30,
fill: 'blue',
});
const circle2 = new Circle({
scaleX: 2,
scaleY: 2,
radius: 10,
top: 120,
left: -20,
fill: 'purple',
});
const deepRect = new Rect({
width: 50,
height: 30,
top: 10,
left: 110,
fill: 'red',
skewX: 40,
skewY: 20,
});
const rect2 = new Rect({
width: 100,
height: 80,
top: 50,
left: 60,
fill: 'green',
});
const deepGroup = new Group([deepTriangle, deepCircle, deepRect], {
subTargetCheck: true,
});
const group2 = new Group([deepGroup, circle2, rect2, triangle2], {
subTargetCheck: true,
});
const group3 = new Group([group2], { subTargetCheck: true });
registerTestObjects({
deepTriangle,
triangle2,
deepCircle,
circle2,
rect2,
deepRect,
deepGroup,
group2,
group3,
});
const canvas = new Canvas(undefined, { perPixelTargetFind: true });
canvas.add(group3);
return {
canvas,
deepTriangle,
triangle2,
deepCircle,
circle2,
rect2,
deepRect,
deepGroup,
group2,
group3,
};
};
test.each([
{ x: 5, y: 5 },
{ x: 21, y: 9 },
{ x: 37, y: 7 },
{ x: 89, y: 47 },
{ x: 16, y: 122 },
{ x: 127, y: 37 },
{ x: 87, y: 139 },
])('transparent hit on %s', ({ x: clientX, y: clientY }) => {
const { canvas } = prepareTest();
expect(findTarget(canvas, { clientX, clientY })).toEqual({
target: undefined,
targets: [],
});
});
test('findTarget with perPixelTargetFind in nested group', () => {
const {
canvas,
deepTriangle,
triangle2,
deepCircle,
circle2,
rect2,
deepRect,
deepGroup,
group2,
group3,
} = prepareTest();
expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({
target: group3,
targets: [deepTriangle, deepGroup, group2],
});
expect(findTarget(canvas, { clientX: 50, clientY: 20 })).toEqual({
target: group3,
targets: [deepCircle, deepGroup, group2],
});
expect(findTarget(canvas, { clientX: 117, clientY: 16 })).toEqual({
target: group3,
targets: [deepRect, deepGroup, group2],
});
expect(findTarget(canvas, { clientX: 100, clientY: 90 })).toEqual({
target: group3,
targets: [rect2, group2],
});
expect(findTarget(canvas, { clientX: 9, clientY: 145 })).toEqual({
target: group3,
targets: [circle2, group2],
});
expect(findTarget(canvas, { clientX: 66, clientY: 143 })).toEqual({
target: group3,
targets: [triangle2, group2],
});
});
});
test('findTarget on active selection', () => {
const rect1 = new FabricObject({
left: 0,
top: 0,
width: 10,
height: 10,
});
const rect2 = new FabricObject({
left: 20,
top: 20,
width: 10,
height: 10,
});
const rect3 = new FabricObject({
left: 20,
top: 0,
width: 10,
height: 10,
});
const activeSelection = new ActiveSelection([rect1, rect2], {
subTargetCheck: true,
cornerSize: 2,
});
registerTestObjects({ rect1, rect2, rect3, activeSelection });
const canvas = new Canvas(undefined, { activeSelection });
canvas.add(rect1, rect2, rect3);
canvas.setActiveObject(activeSelection);
expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({
target: activeSelection,
targets: [rect1],
});
expect(findTarget(canvas, { clientX: 40, clientY: 15 })).toEqual({
target: undefined,
targets: [],
});
expect(activeSelection.__corner).toBeUndefined();
expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({
target: activeSelection,
targets: [],
});
expect(activeSelection.__corner).toBe('tl');
expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual(
{
target: activeSelection,
targets: [],
}
// 'Should not return the rect behind active selection'
);
canvas.discardActiveObject();
expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual(
{
target: rect3,
targets: [],
}
// 'Should return the rect after clearing selection'
);
});
test('findTarget on active selection with perPixelTargetFind', () => {
const rect1 = new Rect({
left: 0,
top: 0,
width: 10,
height: 10,
});
const rect2 = new Rect({
left: 20,
top: 20,
width: 10,
height: 10,
});
const activeSelection = new ActiveSelection([rect1, rect2]);
registerTestObjects({ rect1, rect2, activeSelection });
const canvas = new Canvas(undefined, {
activeSelection,
perPixelTargetFind: true,
preserveObjectStacking: true,
});
canvas.add(rect1, rect2);
canvas.setActiveObject(activeSelection);
expect(findTarget(canvas, { clientX: 8, clientY: 8 })).toEqual({
target: activeSelection,
targets: [],
});
expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({
target: undefined,
targets: [],
});
});
});
it('should fire mouse over/out events on target', () => {
const target = new FabricObject({ width: 10, height: 10 });
const canvas = new Canvas();
canvas.add(target);
jest.spyOn(target, 'toJSON').mockReturnValue('target');
const targetSpy = jest.spyOn(target, 'fire');
const canvasSpy = jest.spyOn(canvas, 'fire');
const enter = new MouseEvent('mousemove', { clientX: 5, clientY: 5 });
const exit = new MouseEvent('mousemove', { clientX: 20, clientY: 20 });
canvas._onMouseMove(enter);
canvas._onMouseMove(exit);
expect(targetSpy.mock.calls).toMatchSnapshot();
expect(canvasSpy.mock.calls).toMatchSnapshot();
});
});

@@ -0,1 +1,2 @@

import { classRegistry } from '../ClassRegistry';
import { NONE } from '../constants';

@@ -11,2 +12,3 @@ import type {

import { Point } from '../Point';
import type { ActiveSelection } from '../shapes/ActiveSelection';
import type { Group } from '../shapes/Group';

@@ -18,2 +20,3 @@ import type { IText } from '../shapes/IText/IText';

import { sendPointToPlane } from '../util/misc/planeChange';
import { isActiveSelection } from '../util/typeAssertions';
import type { CanvasOptions, TCanvasOptions } from './CanvasOptions';

@@ -923,45 +926,3 @@ import { SelectableCanvas } from './SelectableCanvas';

/**
* End the current transform.
* You don't usually need to call this method unless you are interrupting a user initiated transform
* because of some other event ( a press of key combination, or something that block the user UX )
* @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event
*/
endCurrentTransform(e: TPointerEvent) {
const transform = this._currentTransform;
this._finalizeCurrentTransform(e);
if (transform && transform.target) {
// this could probably go inside _finalizeCurrentTransform
transform.target.isMoving = false;
}
this._currentTransform = null;
}
/**
* @private
* @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event
*/
_finalizeCurrentTransform(e: TPointerEvent) {
const transform = this._currentTransform!,
target = transform.target,
options = {
e,
target,
transform,
action: transform.action,
};
if (target._scaling) {
target._scaling = false;
}
target.setCoords();
if (transform.actionPerformed) {
this.fire('object:modified', options);
target.fire('modified', options);
}
}
/**
* @private
* @param {Event} e Event object fired on mousedown

@@ -1104,3 +1065,3 @@ */

control && control.getMouseDownHandler(e, target, control);
if (mouseDownHandler) {
mouseDownHandler &&
mouseDownHandler.call(

@@ -1113,3 +1074,2 @@ control,

);
}
}

@@ -1155,13 +1115,2 @@ }

/**
* @private
*/
_beforeTransform(e: TPointerEvent) {
const t = this._currentTransform!;
this.fire('before:transform', {
e,
transform: t,
});
}
/**
* Method that defines the actions when mouse is hovering the canvas.

@@ -1349,5 +1298,2 @@ * The currentTransform parameter will define whether the user is rotating/scaling/translating

: scenePoint;
// seems used only here.
// @TODO: investigate;
transform.reset = false;
transform.shiftKey = e.shiftKey;

@@ -1397,6 +1343,5 @@ transform.altKey = !!this.centeredKey && e[this.centeredKey];

let hoverCursor = target.hoverCursor || this.hoverCursor;
const activeSelection =
this._activeObject === this._activeSelection
? this._activeObject
: null,
const activeSelection = isActiveSelection(this._activeObject)
? this._activeObject
: null,
// only show proper corner when group selection is not active

@@ -1442,4 +1387,3 @@ corner =

const activeObject = this._activeObject;
const activeSelection = this._activeSelection;
const isAS = activeObject === activeSelection;
const isAS = isActiveSelection(activeObject);
if (

@@ -1467,5 +1411,4 @@ // check if an active object exists on canvas and if the user is pressing the `selectionKey` while canvas supports multi selection.

if (isAS) {
const prevActiveObjects =
activeSelection.getObjects() as FabricObject[];
if (target === activeSelection) {
const prevActiveObjects = activeObject.getObjects();
if (target === activeObject) {
const pointer = this.getViewportPoint(e);

@@ -1483,15 +1426,17 @@ target =

}
if (target.group === activeSelection) {
if (target.group === activeObject) {
// `target` is part of active selection => remove it
activeSelection.remove(target);
activeObject.remove(target);
this._hoveredTarget = target;
this._hoveredTargets = [...this.targets];
if (activeSelection.size() === 1) {
// if after removing an object we are left with one only...
if (activeObject.size() === 1) {
// activate last remaining object
this._setActiveObject(activeSelection.item(0) as FabricObject, e);
// deselecting the active selection will remove the remaining object from it
this._setActiveObject(activeObject.item(0), e);
}
} else {
// `target` isn't part of active selection => add it
activeSelection.multiSelectAdd(target);
this._hoveredTarget = activeSelection;
// `target` isn't part of active selection => add it
activeObject.multiSelectAdd(target);
this._hoveredTarget = activeObject;
this._hoveredTargets = [...this.targets];

@@ -1504,8 +1449,17 @@ }

// add the active object and the target to the active selection and set it as the active object
activeSelection.multiSelectAdd(activeObject, target);
this._hoveredTarget = activeSelection;
const klass =
classRegistry.getClass<typeof ActiveSelection>('ActiveSelection');
const newActiveSelection = new klass([], {
/**
* it is crucial to pass the canvas ref before calling {@link ActiveSelection#multiSelectAdd}
* since it uses {@link FabricObject#isInFrontOf} which relies on the canvas ref
*/
canvas: this,
});
newActiveSelection.multiSelectAdd(activeObject, target);
this._hoveredTarget = newActiveSelection;
// ISSUE 4115: should we consider subTargets here?
// this._hoveredTargets = [];
// this._hoveredTargets = this.targets.concat();
this._setActiveObject(activeSelection, e);
this._setActiveObject(newActiveSelection, e);
this._fireSelectionEvents([activeObject], e);

@@ -1564,4 +1518,5 @@ }

// add to active selection and make it the active object
this._activeSelection.add(...objects);
this.setActiveObject(this._activeSelection, e);
const klass =
classRegistry.getClass<typeof ActiveSelection>('ActiveSelection');
this.setActiveObject(new klass(objects, { canvas: this }), e);
}

@@ -1568,0 +1523,0 @@

import type { ModifierKey, TOptionalModifierKey } from '../EventTypeDefs';
import type { ActiveSelection } from '../shapes/ActiveSelection';
import type { TOptions } from '../typedefs';

@@ -263,5 +262,3 @@ import type { StaticCanvasOptions } from './StaticCanvasOptions';

export type TCanvasOptions = TOptions<
CanvasOptions & { activeSelection: ActiveSelection }
>;
export type TCanvasOptions = TOptions<CanvasOptions>;

@@ -268,0 +265,0 @@ export const canvasDefaults: TOptions<CanvasOptions> = {

@@ -305,2 +305,50 @@ import { FabricObject } from '../shapes/Object/FabricObject';

});
describe('setupCurrentTransform', () => {
test.each(
['tl', 'mt', 'tr', 'mr', 'br', 'mb', 'bl', 'ml', 'mtr']
.map((controlKey) => [
{ controlKey, zoom: false },
{ controlKey, zoom: true },
])
.flat()
)('should fire before:transform event %p', ({ controlKey, zoom }) => {
const canvas = new Canvas();
const canvasOffset = canvas.calcOffset();
const object = new FabricObject({
left: 50,
top: 50,
width: 50,
height: 50,
});
canvas.add(object);
canvas.setActiveObject(object);
zoom && canvas.zoomToPoint(new Point(25, 25), 2);
expect(canvas._currentTransform).toBeFalsy();
const spy = jest.fn();
canvas.on('before:transform', spy);
const setupCurrentTransformSpy = jest.spyOn(
canvas,
'_setupCurrentTransform'
);
const {
corner: { tl, tr, bl },
} = object.oCoords[controlKey];
canvas.getSelectionElement().dispatchEvent(
new MouseEvent('mousedown', {
clientX: canvasOffset.left + (tl.x + tr.x) / 2,
clientY: canvasOffset.top + (tl.y + bl.y) / 2,
which: 1,
})
);
expect(setupCurrentTransformSpy).toHaveBeenCalledTimes(1);
expect(canvas._currentTransform).toBeDefined();
expect(canvas._currentTransform).toHaveProperty('target', object);
expect(canvas._currentTransform).toHaveProperty('corner', controlKey);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});

@@ -14,3 +14,2 @@ import { dragHandler } from '../controls/drag';

addTransformToObject,
resetObjectTransform,
saveObjectTransform,

@@ -35,9 +34,9 @@ } from '../util/misc/objectTransforms';

import { sendPointToPlane } from '../util/misc/planeChange';
import { ActiveSelection } from '../shapes/ActiveSelection';
import { cos, createCanvasElement, sin } from '../util';
import { CanvasDOMManager } from './DOMManagers/CanvasDOMManager';
import { BOTTOM, CENTER, LEFT, RIGHT, TOP } from '../constants';
import type { CanvasOptions, TCanvasOptions } from './CanvasOptions';
import type { CanvasOptions } from './CanvasOptions';
import { canvasDefaults } from './CanvasOptions';
import { Intersection } from '../Intersection';
import { isActiveSelection } from '../util/typeAssertions';

@@ -299,14 +298,3 @@ /**

declare _activeObject?: FabricObject;
protected _activeSelection: ActiveSelection;
constructor(
el?: string | HTMLCanvasElement,
{ activeSelection = new ActiveSelection(), ...options }: TCanvasOptions = {}
) {
super(el, options);
this._activeSelection = activeSelection;
this._activeSelection.set('canvas', this);
this._activeSelection.setCoords();
}
protected initElements(el?: string | HTMLCanvasElement) {

@@ -563,2 +551,7 @@ this.elements = new CanvasDOMManager(el, {

};
if (!controlName) {
return origin;
}
// is a left control ?

@@ -584,3 +577,4 @@ if (['ml', 'tl', 'bl'].includes(controlName)) {

* @param {Event} e Event object
* @param {FaricObject} target
* @param {FabricObject} target
* @param {boolean} [alreadySelected] pass true to setup the active control
*/

@@ -592,5 +586,2 @@ _setupCurrentTransform(

): void {
if (!target) {
return;
}
const pointer = target.group

@@ -604,4 +595,3 @@ ? // transform pointer to target's containing coordinate plane

: this.getScenePoint(e);
const corner = target.getActiveControl() || '',
control = !!corner && target.controls[corner],
const { key: corner = '', control } = target.getActiveControl() || {},
actionHandler =

@@ -612,4 +602,6 @@ alreadySelected && control

action = getActionFromCorner(alreadySelected, corner, e, target),
origin = this._getOriginFromCorner(target, corner),
altKey = e[this.centeredKey as ModifierKey],
origin = this._shouldCenterTransform(target, action, altKey)
? ({ x: CENTER, y: CENTER } as const)
: this._getOriginFromCorner(target, corner),
/**

@@ -621,3 +613,3 @@ * relative to target's containing coordinate plane

target: target,
action: action,
action,
actionHandler,

@@ -642,3 +634,3 @@ actionPerformed: false,

shiftKey: e.shiftKey,
altKey: altKey,
altKey,
original: {

@@ -651,9 +643,8 @@ ...saveObjectTransform(target),

if (this._shouldCenterTransform(target, action, altKey)) {
transform.originX = CENTER;
transform.originY = CENTER;
}
this._currentTransform = transform;
// @ts-expect-error this method exists in the subclass - should be moved or declared as abstract
this._beforeTransform(e);
this.fire('before:transform', {
e,
transform,
});
}

@@ -1044,9 +1035,2 @@

/**
* Returns instance's active selection
*/
getActiveSelection() {
return this._activeSelection;
}
/**
* Returns an array with the current selected objects

@@ -1057,10 +1041,7 @@ * @return {FabricObject[]} active objects array

const active = this._activeObject;
if (active) {
if (active === this._activeSelection) {
return [...(active as ActiveSelection)._objects];
} else {
return [active];
}
}
return [];
return isActiveSelection(active)
? active.getObjects()
: active
? [active]
: [];
}

@@ -1150,5 +1131,7 @@

_setActiveObject(object: FabricObject, e?: TPointerEvent) {
if (this._activeObject === object) {
const prevActiveObject = this._activeObject;
if (prevActiveObject === object) {
return false;
}
// after calling this._discardActiveObject, this,_activeObject could be undefined
if (!this._discardActiveObject(e, object) && this._activeObject) {

@@ -1161,6 +1144,6 @@ // refused to deselect

}
this._activeObject = object;
if (object instanceof ActiveSelection && this._activeSelection !== object) {
this._activeSelection = object;
if (isActiveSelection(object) && prevActiveObject !== object) {
object.set('canvas', this);

@@ -1191,9 +1174,3 @@ object.setCoords();

}
// clear active selection
if (obj === this._activeSelection) {
this._activeSelection.removeAll();
resetObjectTransform(this._activeSelection);
}
if (this._currentTransform && this._currentTransform.target === obj) {
// @ts-expect-error this method exists in the subclass - should be moved or declared as abstract
this.endCurrentTransform(e);

@@ -1230,2 +1207,44 @@ }

/**
* End the current transform.
* You don't usually need to call this method unless you are interrupting a user initiated transform
* because of some other event ( a press of key combination, or something that block the user UX )
* @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event
*/
endCurrentTransform(e?: TPointerEvent) {
const transform = this._currentTransform;
this._finalizeCurrentTransform(e);
if (transform && transform.target) {
// this could probably go inside _finalizeCurrentTransform
transform.target.isMoving = false;
}
this._currentTransform = null;
}
/**
* @private
* @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event
*/
_finalizeCurrentTransform(e?: TPointerEvent) {
const transform = this._currentTransform!,
target = transform.target,
options = {
e,
target,
transform,
action: transform.action,
};
if (target._scaling) {
target._scaling = false;
}
target.setCoords();
if (transform.actionPerformed) {
this.fire('object:modified', options);
target.fire('modified', options);
}
}
/**
* Sets viewport transformation of this canvas instance

@@ -1247,8 +1266,10 @@ * @param {Array} vpt a Canvas 2D API transform matrix

// dispose of active selection
const activeSelection = this._activeSelection;
activeSelection.removeAll();
// @ts-expect-error disposing
this._activeSelection = undefined;
activeSelection.dispose();
const activeObject = this._activeObject;
if (isActiveSelection(activeObject)) {
activeObject.removeAll();
activeObject.dispose();
}
delete this._activeObject;
super.destroy();

@@ -1292,3 +1313,3 @@

*/
_toObject(
protected _toObject(
instance: FabricObject,

@@ -1315,10 +1336,7 @@ methodName: 'toObject' | 'toDatalessObject',

*/
_realizeGroupTransformOnObject(
private _realizeGroupTransformOnObject(
instance: FabricObject
): Partial<typeof instance> {
if (
instance.group &&
instance.group === this._activeSelection &&
this._activeObject === instance.group
) {
const { group } = instance;
if (group && isActiveSelection(group) && this._activeObject === group) {
const layoutProps = [

@@ -1336,3 +1354,3 @@ 'angle',

const originalValues = pick<typeof instance>(instance, layoutProps);
addTransformToObject(instance, this._activeObject.calcOwnMatrix());
addTransformToObject(instance, group.calcOwnMatrix());
return originalValues;

@@ -1339,0 +1357,0 @@ } else {

@@ -46,2 +46,3 @@ import { config } from '../config';

import { log, FabricError } from '../util/internals/console';
import { getDevicePixelRatio } from '../env';

@@ -235,13 +236,7 @@ export type TCanvasSizeOptions = {

* @private
*/
_isRetinaScaling() {
return config.devicePixelRatio > 1 && this.enableRetinaScaling;
}
/**
* @private
* @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html
* @return {Number} retinaScaling if applied, otherwise 1;
*/
getRetinaScaling() {
return this._isRetinaScaling() ? Math.max(1, config.devicePixelRatio) : 1;
return this.enableRetinaScaling ? getDevicePixelRatio() : 1;
}

@@ -864,3 +859,3 @@

*/
_toObject(
protected _toObject(
instance: FabricObject,

@@ -867,0 +862,0 @@ methodName: TValidToObjectMethod,

@@ -27,3 +27,3 @@ import { FabricError } from './util/internals/console';

getClass(classType: string): any {
getClass<T>(classType: string): T {
const constructor = this[JSON].get(classType);

@@ -30,0 +30,0 @@ if (!constructor) {

@@ -19,6 +19,13 @@ import { Canvas } from '../canvas/Canvas';

controls: { test: control, test2: control },
canvas: new Canvas(),
});
jest.spyOn(target, '_findTargetCorner').mockReturnValue('test');
jest.spyOn(target, 'getActiveControl').mockReturnValue('test');
target.setCoords();
jest
.spyOn(target, '_findTargetCorner')
.mockImplementation(function (this: FabricObject) {
return (this.__corner = 'test');
});
const canvas = new Canvas();

@@ -25,0 +32,0 @@ canvas.setActiveObject(target);

import { Point } from '../Point';
import { Control } from './Control';
import type { TMat2D } from '../typedefs';
import { CENTER, iMatrix } from '../constants';
import type { Polyline } from '../shapes/Polyline';

@@ -13,4 +12,4 @@ import { multiplyTransformMatrices } from '../util/misc/matrix';

} from '../EventTypeDefs';
import { getLocalPoint } from './util';
import { wrapWithFireEvent } from './wrapWithFireEvent';
import { sendPointToPlane } from '../util';

@@ -21,6 +20,2 @@ const ACTION_NAME: TModificationEvents = 'modifyPoly';

const getSize = (poly: Polyline) => {
return new Point(poly.width, poly.height);
};
/**

@@ -32,10 +27,11 @@ * This function locates the controls.

return function (dim: Point, finalMatrix: TMat2D, polyObject: Polyline) {
const x = polyObject.points[pointIndex].x - polyObject.pathOffset.x,
y = polyObject.points[pointIndex].y - polyObject.pathOffset.y;
return new Point(x, y).transform(
multiplyTransformMatrices(
polyObject.canvas?.viewportTransform ?? iMatrix,
polyObject.calcTransformMatrix()
)
);
const { points, pathOffset } = polyObject;
return new Point(points[pointIndex])
.subtract(pathOffset)
.transform(
multiplyTransformMatrices(
polyObject.getViewportTransform(),
polyObject.calcTransformMatrix()
)
);
};

@@ -57,16 +53,11 @@ };

) => {
const poly = transform.target as Polyline,
pointIndex = transform.pointIndex,
mouseLocalPosition = getLocalPoint(transform, CENTER, CENTER, x, y),
polygonBaseSize = getSize(poly),
size = poly._getTransformedDimensions(),
sizeFactor = polygonBaseSize.divide(size),
adjustFlip = new Point(poly.flipX ? -1 : 1, poly.flipY ? -1 : 1);
const { target, pointIndex } = transform;
const poly = target as Polyline;
const mouseLocalPosition = sendPointToPlane(
new Point(x, y),
undefined,
poly.calcOwnMatrix()
);
const finalPointPosition = mouseLocalPosition
.multiply(adjustFlip)
.multiply(sizeFactor)
.add(poly.pathOffset);
poly.points[pointIndex] = finalPointPosition;
poly.points[pointIndex] = mouseLocalPosition.add(poly.pathOffset);
poly.setDimensions();

@@ -97,15 +88,11 @@

.transform(poly.calcOwnMatrix()),
actionPerformed = fn(eventData, { ...transform, pointIndex }, x, y),
adjustFlip = new Point(poly.flipX ? -1 : 1, poly.flipY ? -1 : 1);
actionPerformed = fn(eventData, { ...transform, pointIndex }, x, y);
const newPositionNormalized = anchorPoint
const newAnchorPointInParentPlane = anchorPoint
.subtract(poly.pathOffset)
.divide(poly._getNonTransformedDimensions())
.multiply(adjustFlip);
.transform(poly.calcOwnMatrix());
poly.setPositionByOrigin(
anchorPointInParentPlane,
newPositionNormalized.x + 0.5,
newPositionNormalized.y + 0.5
);
const diff = newAnchorPointInParentPlane.subtract(anchorPointInParentPlane);
poly.left -= diff.x;
poly.top -= diff.y;

@@ -112,0 +99,0 @@ return actionPerformed;

@@ -73,2 +73,11 @@ import type {

}
// code crashes because of a division by 0 if a 0 sized object is scaled
// forbid to prevent scaling to happen. ISSUE-9475
const { width, height, strokeWidth } = fabricObject;
if (width === 0 && strokeWidth === 0 && by !== 'y') {
return true;
}
if (height === 0 && strokeWidth === 0 && by !== 'x') {
return true;
}
return false;

@@ -75,0 +84,0 @@ }

@@ -28,3 +28,3 @@ import type {

alreadySelected: boolean,
corner: string,
corner: string | undefined,
e: TPointerEvent,

@@ -31,0 +31,0 @@ target: FabricObject

@@ -7,17 +7,10 @@ /* eslint-disable no-restricted-globals */

let initialized = false;
let isTouchSupported: boolean;
export const getEnv = (): TFabricEnv => {
if (!initialized) {
isTouchSupported =
'ontouchstart' in window ||
'ontouchstart' in document ||
(window && window.navigator && window.navigator.maxTouchPoints > 0);
initialized = true;
}
return {
document,
window,
isTouchSupported,
isTouchSupported:
'ontouchstart' in window ||
'ontouchstart' in document ||
(window && window.navigator && window.navigator.maxTouchPoints > 0),
WebGLProbe: new WebGLProbe(),

@@ -24,0 +17,0 @@ dispose() {

/**
* This file is consumed by fabric.
* The `./node` and `./browser` files define the env variable that is used by this module.
* The `./node` module sets the env at import time.
* The `./browser` module is defined to be the default env and doesn't set the env at all.

@@ -10,4 +9,5 @@ * This is done in order to support isomorphic usage for browser and node applications

import { config } from '../config';
import { getEnv as getBrowserEnv } from './browser';
import type { TFabricEnv } from './types';
import { getEnv as getBrowserEnv } from './browser';
import type { DOMWindow } from 'jsdom';

@@ -34,3 +34,6 @@

export const getEnv = () => env || getBrowserEnv();
/**
* In order to support SSR we **MUST** access the browser env only after the window has loaded
*/
export const getEnv = () => env || (env = getBrowserEnv());

@@ -41,1 +44,7 @@ export const getFabricDocument = (): Document => getEnv().document;

getEnv().window;
/**
* @returns the config value if defined, fallbacks to the environment value
*/
export const getDevicePixelRatio = () =>
Math.max(config.devicePixelRatio ?? getFabricWindow().devicePixelRatio, 1);

@@ -6,5 +6,3 @@ /* eslint-disable no-restricted-globals */

import utils from 'jsdom/lib/jsdom/living/generated/utils.js';
import { config } from '../config';
import { NodeGLProbe } from '../filters/GLProbes/NodeGLProbe';
import { setEnv } from './index';
import type { TCopyPasteData, TFabricEnv } from './types';

@@ -27,6 +25,2 @@

config.configure({
devicePixelRatio: JSDOMWindow.devicePixelRatio || 1,
});
export const getNodeCanvas = (canvasEl: HTMLCanvasElement) => {

@@ -59,3 +53,1 @@ const impl = jsdomImplForWrapper(canvasEl);

};
setEnv(getEnv());

@@ -10,5 +10,5 @@ import type { GLProbe } from '../filters/GLProbes/GLProbe';

export type TFabricEnv = {
document: Document;
window: (Window & typeof globalThis) | DOMWindow;
isTouchSupported: boolean;
readonly document: Document;
readonly window: (Window & typeof globalThis) | DOMWindow;
readonly isTouchSupported: boolean;
WebGLProbe: GLProbe;

@@ -15,0 +15,0 @@ dispose(element: Element): void;

@@ -58,3 +58,3 @@ import type { Control } from './controls/Control';

target: FabricObject;
action: string;
action?: string;
actionHandler?: TransformActionHandler;

@@ -83,4 +83,2 @@ corner: string;

};
// @TODO: investigate if this reset is really needed
reset?: boolean;
actionPerformed: boolean;

@@ -111,7 +109,7 @@ };

export interface ModifiedEvent<E extends Event = TPointerEvent>
extends TEvent<E> {
export interface ModifiedEvent<E extends Event = TPointerEvent> {
e?: E;
transform: Transform;
target: FabricObject;
action: string;
action?: string;
}

@@ -131,2 +129,4 @@

BasicTransformEvent & { target: FabricObject },
// TODO: this typing makes not possible to use properties from modified event
// in object:modified
ModifiedEvent | { target: FabricObject }

@@ -133,0 +133,0 @@ > & {

@@ -70,3 +70,5 @@ import { BaseFilter } from './BaseFilter';

((object.subFilters || []) as BaseFilter[]).map((filter) =>
classRegistry.getClass(filter.type).fromObject(filter, options)
classRegistry
.getClass<typeof BaseFilter>(filter.type)
.fromObject(filter, options)
)

@@ -73,0 +75,0 @@ ).then(

@@ -41,2 +41,3 @@ import { log } from '../../util/internals/console';

);
gl.getExtension('WEBGL_lose_context')!.loseContext();
log('log', `WebGL: max texture size ${this.maxTextureSize}`);

@@ -43,0 +44,0 @@ }

@@ -10,4 +10,4 @@ export * as filters from './filters';

export { WebGLFilterBackend } from './WebGLFilterBackend';
export { isWebGLPipelineState } from './utils';
export { isWebGLPipelineState, isPutImageFaster } from './utils';
export * from './typedefs';

@@ -0,1 +1,4 @@

import { getFabricWindow } from '../env';
import { createCanvasElement } from '../util/misc/dom';
import { WebGLFilterBackend } from './WebGLFilterBackend';
import type { TWebGLPipelineState, T2DPipelineState } from './typedefs';

@@ -8,1 +11,45 @@

};
/**
* Pick a method to copy data from GL context to 2d canvas. In some browsers using
* drawImage should be faster, but is also bugged for a small combination of old hardware
* and drivers.
* putImageData is faster than drawImage for that specific operation.
*/
export const isPutImageFaster = (width: number, height: number): boolean => {
const targetCanvas = createCanvasElement();
const sourceCanvas = createCanvasElement();
const gl = sourceCanvas.getContext('webgl')!;
// eslint-disable-next-line no-undef
const imageBuffer = new ArrayBuffer(width * height * 4);
const testContext = {
imageBuffer: imageBuffer,
} as unknown as Required<WebGLFilterBackend>;
const testPipelineState = {
destinationWidth: width,
destinationHeight: height,
targetCanvas: targetCanvas,
} as unknown as TWebGLPipelineState;
let startTime;
targetCanvas.width = width;
targetCanvas.height = height;
startTime = getFabricWindow().performance.now();
WebGLFilterBackend.prototype.copyGLTo2D.call(
testContext,
gl,
testPipelineState
);
const drawImageTime = getFabricWindow().performance.now() - startTime;
startTime = getFabricWindow().performance.now();
WebGLFilterBackend.prototype.copyGLTo2DPutImageData.call(
testContext,
gl,
testPipelineState
);
const putImageDataTime = getFabricWindow().performance.now() - startTime;
return drawImageTime > putImageDataTime;
};

@@ -1,2 +0,1 @@

import { getFabricWindow } from '../env';
import { config } from '../config';

@@ -69,49 +68,5 @@ import { createCanvasElement } from '../util/misc/dom';

this.createWebGLCanvas(width, height);
// eslint-disable-next-line
this.chooseFastestCopyGLTo2DMethod(width, height);
}
/**
* Pick a method to copy data from GL context to 2d canvas. In some browsers using
* drawImage should be faster, but is also bugged for a small combination of old hardware
* and drivers.
* putImageData is faster than drawImage for that specific operation.
*/
chooseFastestCopyGLTo2DMethod(width: number, height: number): void {
const targetCanvas = createCanvasElement();
// eslint-disable-next-line no-undef
const imageBuffer = new ArrayBuffer(width * height * 4);
if (config.forceGLPutImageData) {
this.imageBuffer = imageBuffer;
this.copyGLTo2D = copyGLTo2DPutImageData;
return;
}
const testContext = {
imageBuffer: imageBuffer,
} as unknown as Required<WebGLFilterBackend>;
const testPipelineState = {
destinationWidth: width,
destinationHeight: height,
targetCanvas: targetCanvas,
} as unknown as TWebGLPipelineState;
let startTime;
targetCanvas.width = width;
targetCanvas.height = height;
startTime = getFabricWindow().performance.now();
this.copyGLTo2D.call(testContext, this.gl, testPipelineState);
const drawImageTime = getFabricWindow().performance.now() - startTime;
startTime = getFabricWindow().performance.now();
copyGLTo2DPutImageData.call(testContext, this.gl, testPipelineState);
const putImageDataTime = getFabricWindow().performance.now() - startTime;
if (drawImageTime > putImageDataTime) {
this.imageBuffer = imageBuffer;
this.copyGLTo2D = copyGLTo2DPutImageData;
}
}
/**
* Create a canvas element and associated WebGL context and attaches them as

@@ -393,2 +348,31 @@ * class properties to the GLFilterBackend class.

/**
* Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData
* API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra).
*
* @param {WebGLRenderingContext} sourceContext The WebGL context to copy from.
* @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to.
* @param {Object} pipelineState The 2D target canvas to copy on to.
*/
copyGLTo2DPutImageData(
this: Required<WebGLFilterBackend>,
gl: WebGLRenderingContext,
pipelineState: TWebGLPipelineState
) {
const targetCanvas = pipelineState.targetCanvas,
ctx = targetCanvas.getContext('2d'),
dWidth = pipelineState.destinationWidth,
dHeight = pipelineState.destinationHeight,
numBytes = dWidth * dHeight * 4;
if (!ctx) {
return;
}
const u8 = new Uint8Array(this.imageBuffer, 0, numBytes);
const u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes);
gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8);
const imgData = new ImageData(u8Clamped, dWidth, dHeight);
ctx.putImageData(imgData, 0, 0);
}
/**
* Attempt to extract GPU information strings from a WebGL context.

@@ -437,30 +421,1 @@ *

}
/**
* Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData
* API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra).
*
* @param {WebGLRenderingContext} sourceContext The WebGL context to copy from.
* @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to.
* @param {Object} pipelineState The 2D target canvas to copy on to.
*/
function copyGLTo2DPutImageData(
this: Required<WebGLFilterBackend>,
gl: WebGLRenderingContext,
pipelineState: TWebGLPipelineState
) {
const targetCanvas = pipelineState.targetCanvas,
ctx = targetCanvas.getContext('2d'),
dWidth = pipelineState.destinationWidth,
dHeight = pipelineState.destinationHeight,
numBytes = dWidth * dHeight * 4;
if (!ctx) {
return;
}
const u8 = new Uint8Array(this.imageBuffer, 0, numBytes);
const u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes);
gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8);
const imgData = new ImageData(u8Clamped, dWidth, dHeight);
ctx.putImageData(imgData, 0, 0);
}

@@ -186,16 +186,2 @@ import type { TModificationEvents } from '../EventTypeDefs';

it('a non attached manager should not subscribe object', () => {
const manager = new LayoutManager();
const subscribe = jest.spyOn(manager, 'subscribe');
const object = new FabricObject();
const target = new Group([object]);
manager.performLayout({
type: LAYOUT_TYPE_INITIALIZATION,
target,
targets: [object],
});
expect(subscribe).not.toHaveBeenCalled();
});
it.each([

@@ -309,3 +295,4 @@ { trigger: LAYOUT_TYPE_INITIALIZATION, action: 'subscribe' },

const target = new Group([], { scaleX: 2, scaleY: 0.5, angle: 30 });
const rect = new FabricObject({ width: 50, height: 50 });
const target = new Group([rect], { scaleX: 2, scaleY: 0.5, angle: 30 });

@@ -316,2 +303,3 @@ const context: StrictLayoutContext = {

target,
targets: [rect],
...options,

@@ -323,3 +311,9 @@ stopPropagation() {

expect(manager['getLayoutResult'](context)).toMatchSnapshot();
expect(manager['getLayoutResult'](context)).toMatchSnapshot({
cloneDeepWith: (value: any) => {
if (value instanceof Point) {
return new Point(Math.round(value.x), Math.round(value.y));
}
},
});
});

@@ -482,8 +476,2 @@ });

const shouldResetTransform = jest
.spyOn(manager.strategy, 'shouldResetTransform')
.mockImplementation(() => {
lifecycle.push(shouldResetTransform);
});
const context: StrictLayoutContext = {

@@ -509,7 +497,5 @@ bubbles,

expect(lifecycle).toEqual([
shouldResetTransform,
targetFire,
...(bubbles ? [parentPerformLayout] : []),
]);
expect(shouldResetTransform).toBeCalledWith(context);
expect(targetFire).toBeCalledWith('layout:after', {

@@ -536,30 +522,2 @@ context,

test.each([true, false])('reset target transform %s', (reset) => {
const targets = [new Group([new FabricObject()]), new FabricObject()];
const target = new Group(targets);
target.left = 50;
const manager = new LayoutManager();
jest
.spyOn(manager.strategy, 'shouldResetTransform')
.mockImplementation(() => {
return reset;
});
const context: StrictLayoutContext = {
bubbles: true,
strategy: manager.strategy,
type: LAYOUT_TYPE_REMOVED,
target,
targets,
prevStrategy: undefined,
stopPropagation() {
this.bubbles = false;
},
};
manager['onAfterLayout'](context);
expect(target.left).toBe(reset ? 0 : 50);
});
test('bubbling', () => {

@@ -566,0 +524,0 @@ const manager = new LayoutManager();

@@ -7,3 +7,2 @@ import type { TModificationEvents } from '../EventTypeDefs';

import { invertTransform } from '../util/misc/matrix';
import { resetObjectTransform } from '../util/misc/objectTransforms';
import { resolveOrigin } from '../util/misc/resolveOrigin';

@@ -118,6 +117,4 @@ import { FitContentLayout } from './LayoutStrategies/FitContentLayout';

if (
// subscribe only if instance is the target's `layoutManager`
target.layoutManager === this &&
(context.type === LAYOUT_TYPE_INITIALIZATION ||
context.type === LAYOUT_TYPE_ADDED)
context.type === LAYOUT_TYPE_INITIALIZATION ||
context.type === LAYOUT_TYPE_ADDED
) {

@@ -267,7 +264,2 @@ context.targets.forEach((object) => this.subscribe(object, context));

const { canvas } = target;
if (strategy.shouldResetTransform(context)) {
resetObjectTransform(target);
target.left = 0;
target.top = 0;
}

@@ -274,0 +266,0 @@ // fire layout event (event will fire only for layouts after initialization layout)

@@ -65,9 +65,2 @@ import { Point } from '../../Point';

/**
* called from the `onAfterLayout` hook
*/
shouldResetTransform(context: StrictLayoutContext) {
return context.target.size() === 0;
}
/**
* Override this method to customize layout.

@@ -74,0 +67,0 @@ */

@@ -29,9 +29,6 @@ import type { CSSRules } from './typedefs';

.split('}')
// remove empty rules.
.filter(function (rule) {
return rule.trim();
})
// remove empty rules and remove everything if we didn't split in at least 2 pieces
.filter((rule, index, array) => array.length > 1 && rule.trim())
// at this point we have hopefully an array of rules `body { style code... `
// eslint-disable-next-line no-loop-func
.forEach(function (rule) {
.forEach((rule) => {
const match = rule.split('{'),

@@ -38,0 +35,0 @@ ruleObj: Record<string, string> = {},

@@ -16,32 +16,3 @@ import { FitContentLayout } from '../LayoutManager';

it('clearing active selection objects resets transform', () => {
const obj = new FabricObject({
left: 100,
top: 100,
width: 100,
height: 100,
});
const selection = new ActiveSelection([obj], {
left: 200,
top: 200,
angle: 45,
skewX: 0.5,
skewY: -0.5,
});
selection.remove(obj);
expect(selection).toMatchObject({
left: 0,
top: 0,
angle: 0,
scaleX: 1,
scaleY: 1,
skewX: 0,
skewY: 0,
flipX: false,
flipY: false,
_objects: [],
});
});
it('deselect removes all objects and resets transform', () => {
it('deselect removes all objects', () => {
const selection = new ActiveSelection([], {

@@ -56,5 +27,5 @@ left: 200,

expect(selection).toMatchObject({
left: 0,
top: 0,
angle: 0,
left: 200,
top: 100,
angle: 45,
scaleX: 1,

@@ -69,7 +40,7 @@ scaleY: 1,

selection.add(new FabricObject({ left: 50, top: 50, strokeWidth: 0 }));
expect(selection.item(0).getCenterPoint()).toEqual({ x: 50, y: 50 });
const { x, y } = selection.item(0).getCenterPoint();
expect({ x: Math.round(x), y: Math.round(y) }).toEqual({ x: 50, y: 50 });
});
// remove skip once #9152 is merged
it.skip('should not set coords in the constructor', () => {
it('should not set coords in the constructor', () => {
const spy = jest.spyOn(ActiveSelection.prototype, 'setCoords');

@@ -87,17 +58,2 @@ new ActiveSelection([

it('sets coords after attaching to canvas', () => {
const canvas = new Canvas(undefined, {
activeSelection: new ActiveSelection([
new FabricObject({
left: 100,
top: 100,
width: 100,
height: 100,
}),
]),
viewportTransform: [2, 0, 0, 0.5, 400, 150],
});
expect(canvas.getActiveSelection().aCoords).toMatchSnapshot();
});
it('`setActiveObject` should update the active selection ref on canvas if it changed', () => {

@@ -111,3 +67,3 @@ const canvas = new Canvas();

canvas.setActiveObject(activeSelection);
expect(canvas.getActiveSelection()).toBe(activeSelection);
expect(canvas.getActiveObject()).toBe(activeSelection);
expect(canvas.getActiveObjects()).toEqual([obj1, obj2]);

@@ -125,4 +81,3 @@ expect(spy).toHaveBeenCalled();

const group = new Group([object]);
const canvas = new Canvas();
const activeSelection = canvas.getActiveSelection();
const activeSelection = new ActiveSelection();

@@ -141,3 +96,2 @@ const eventsSpy = jest.spyOn(object, 'fire');

expect(object.parent).toBe(group);
expect(object.canvas).toBe(canvas);
expect(removeSpy).not.toBeCalled();

@@ -156,3 +110,2 @@ expect(exitSpy).toBeCalledWith(object);

expect(object.parent).toBe(group);
expect(object.canvas).toBeUndefined();
});

@@ -159,0 +112,0 @@

@@ -19,3 +19,2 @@ import type { ControlRenderingStyleOverride } from '../controls/controlRendering';

* Used by Canvas to manage selection.
* Canvas accepts an `activeSelection` option allowing overriding and customization.
*

@@ -27,7 +26,16 @@ * @example

*
* const canvas = new Canvas(el, {
* activeSelection: new MyActiveSelection()
* })
* // override the default `ActiveSelection` class
* classRegistry.setClass(MyActiveSelection)
*/
export class ActiveSelection extends Group {
static type = 'ActiveSelection';
static ownDefaults: Record<string, any> = {
multiSelectionStacking: 'canvas-stacking',
};
static getDefaults() {
return { ...super.getDefaults(), ...this.ownDefaults };
}
/**

@@ -40,7 +48,4 @@ * controls how selected objects are added during a multiselection event

*/
// TODO FIX THIS WITH THE DEFAULTS LOGIC
multiSelectionStacking: MultiSelectionStacking = 'canvas-stacking';
declare multiSelectionStacking: MultiSelectionStacking;
static type = 'ActiveSelection';
/**

@@ -147,5 +152,3 @@ * @private

/**
* If returns true, deselection is cancelled.
* @since 2.0.0
* @return {Boolean} [cancel]
* @override remove all objects
*/

@@ -152,0 +155,0 @@ onDeselect() {

@@ -31,2 +31,3 @@ import type { CollectionEvents, ObjectEvents } from '../EventTypeDefs';

import type { SerializedLayoutManager } from '../LayoutManager/LayoutManager';
import type { FitContentLayout } from '../LayoutManager';

@@ -665,4 +666,8 @@ /**

if (layoutManager) {
const layoutClass = classRegistry.getClass(layoutManager.type);
const strategyClass = classRegistry.getClass(layoutManager.strategy);
const layoutClass = classRegistry.getClass<typeof LayoutManager>(
layoutManager.type
);
const strategyClass = classRegistry.getClass<typeof FitContentLayout>(
layoutManager.strategy
);
group.layoutManager = new layoutClass(new strategyClass());

@@ -669,0 +674,0 @@ } else {

@@ -125,3 +125,2 @@ import type { Canvas } from '../../canvas/Canvas';

const diff = pointer.subtract(pos);
const enableRetinaScaling = canvas._isRetinaScaling();
const retinaScaling = target.getCanvasRetinaScaling();

@@ -145,3 +144,3 @@ const bbox = target.getBoundingRect();

const dragImage = target.toCanvasElement({
enableRetinaScaling,
enableRetinaScaling: canvas.enableRetinaScaling,
viewportTransform: true,

@@ -148,0 +147,0 @@ });

@@ -17,3 +17,3 @@ import type {

import { LEFT, RIGHT, reNewline } from '../../constants';
import type { Canvas } from '../../canvas/Canvas';
import type { IText } from './IText';

@@ -694,4 +694,5 @@ /**

if (this.canvas) {
// @ts-expect-error in reality it is an IText instance
this.canvas.fire('text:editing:exited', { target: this });
this.canvas.fire('text:editing:exited', {
target: this as unknown as IText,
});
isTextChanged && this.canvas.fire('object:modified', { target: this });

@@ -810,5 +811,6 @@ }

const newLineStyles: { [index: number]: TextStyleDeclaration } = {};
const isEndOfLine =
this._unwrappedTextLines[lineIndex].length === charIndex;
let somethingAdded = false;
const originalLineLength = this._unwrappedTextLines[lineIndex].length;
const isEndOfLine = originalLineLength === charIndex;
let someStyleIsCarryingOver = false;
qty || (qty = 1);

@@ -825,3 +827,3 @@ this.shiftLineStyles(lineIndex, qty);

if (numIndex >= charIndex) {
somethingAdded = true;
someStyleIsCarryingOver = true;
newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];

@@ -835,3 +837,3 @@ // remove lines from the previous line since they're on a new line now

let styleCarriedOver = false;
if (somethingAdded && !isEndOfLine) {
if (someStyleIsCarryingOver && !isEndOfLine) {
// if is end of line, the extra style we copied

@@ -842,4 +844,6 @@ // is probably not something we want

}
if (styleCarriedOver) {
if (styleCarriedOver || originalLineLength > charIndex) {
// skip the last line of since we already prepared it.
// or contains text without style that we don't want to style
// just because it changed lines
qty--;

@@ -846,0 +850,0 @@ }

@@ -93,3 +93,3 @@ import { config } from '../../config';

compositionupdate: 'onCompositionUpdate',
onCompositionUpdate: 'onCompositionEnd',
compositionend: 'onCompositionEnd',
} as Record<string, keyof this>).map(([eventName, handler]) =>

@@ -96,0 +96,0 @@ textarea.addEventListener(

@@ -0,4 +1,5 @@

import { Canvas } from '../../canvas/Canvas';
import { Control } from '../../controls/Control';
import { radiansToDegrees } from '../../util';
import { Group } from '../Group';
import { Canvas } from '../../canvas/Canvas';
import { FabricObject } from './FabricObject';

@@ -17,2 +18,3 @@ import { InteractiveFabricObject, type TOCoord } from './InteractiveObject';

});
describe('setCoords for objects inside group with rotation', () => {

@@ -53,2 +55,16 @@ it('all corners are rotated as much as the object total angle', () => {

});
test('getActiveControl', () => {
const object = new FabricObject({ canvas: new Canvas() });
const control = new Control();
object.controls = { control };
object.setCoords();
expect(object.getActiveControl()).toBeUndefined();
object.__corner = 'control';
expect(object.getActiveControl()).toEqual({
key: 'control',
control,
coord: object.oCoords.control,
});
});
});

@@ -156,5 +156,10 @@ import { Point, ZERO } from '../../Point';

if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) {
const target = targetCanvas._currentTransform.target,
action = targetCanvas._currentTransform.action;
if (this === (target as unknown as this) && action.startsWith('scale')) {
const transform = targetCanvas._currentTransform,
target = transform.target,
action = transform.action;
if (
this === (target as unknown as this) &&
action &&
action.startsWith('scale')
) {
return false;

@@ -167,3 +172,10 @@ }

getActiveControl() {
return this.__corner;
const key = this.__corner;
return key
? {
key,
control: this.controls[key],
coord: this.oCoords[key],
}
: undefined;
}

@@ -170,0 +182,0 @@

@@ -55,3 +55,3 @@ import { cache } from '../../cache';

import type { ObjectProps } from './types/ObjectProps';
import { getEnv } from '../../env';
import { getDevicePixelRatio, getEnv } from '../../env';
import { log } from '../../util/internals/console';

@@ -1346,3 +1346,3 @@

// TODO: how to import Image w/o an import cycle?
const ImageClass = classRegistry.getClass('image');
const ImageClass = classRegistry.getClass<typeof FabricImage>('image');
return new ImageClass(canvasEl);

@@ -1370,5 +1370,3 @@ }

abs = Math.abs,
retinaScaling = options.enableRetinaScaling
? Math.max(config.devicePixelRatio, 1)
: 1,
retinaScaling = options.enableRetinaScaling ? getDevicePixelRatio() : 1,
multiplier = (options.multiplier || 1) * retinaScaling;

@@ -1375,0 +1373,0 @@ delete this.group;

@@ -166,11 +166,15 @@ import { config } from '../config';

),
offsetX = bbox.left + bbox.width / 2,
offsetY = bbox.top + bbox.height / 2,
pathOffsetX = offsetX - offsetY * Math.tan(degreesToRadians(this.skewX)),
pathOffsetY =
offsetY - pathOffsetX * Math.tan(degreesToRadians(this.skewY)),
scale = new Point(this.scaleX, this.scaleY);
let offsetX = bbox.left + bbox.width / 2,
offsetY = bbox.top + bbox.height / 2;
if (this.exactBoundingBox) {
offsetX = offsetX - offsetY * Math.tan(degreesToRadians(this.skewX));
// Order of those assignments is important.
// offsetY relies on offsetX being already changed by the line above
offsetY = offsetY - offsetX * Math.tan(degreesToRadians(this.skewY));
}
return {
...bbox,
pathOffset: new Point(pathOffsetX, pathOffsetY),
pathOffset: new Point(offsetX, offsetY),
strokeOffset: new Point(bboxNoStroke.left, bboxNoStroke.top)

@@ -177,0 +181,0 @@ .subtract(new Point(bbox.left, bbox.top))

@@ -9,2 +9,3 @@ import type { ObjectEvents } from '../../EventTypeDefs';

import { pick } from '../../util';
import { pickBy } from '../../util/misc/pick';

@@ -185,3 +186,3 @@ export type CompleteTextStyleDeclaration = Pick<

private _extendStyles(index: number, styles: TextStyleDeclaration): void {
private _extendStyles(index: number, style: TextStyleDeclaration): void {
const { lineIndex, charIndex } = this.get2DCursorLocation(index);

@@ -193,7 +194,14 @@

if (!Object.keys(this._getStyleDeclaration(lineIndex, charIndex)).length) {
this._setStyleDeclaration(lineIndex, charIndex, {});
}
const newStyle = pickBy(
{
// first create a new object that is a merge of existing and new
...this._getStyleDeclaration(lineIndex, charIndex),
...style,
// use the predicate to discard undefined values
},
(value) => value !== undefined
);
Object.assign(this._getStyleDeclaration(lineIndex, charIndex), styles);
// finally assign to the old position the new style
this._setStyleDeclaration(lineIndex, charIndex, newStyle);
}

@@ -249,7 +257,11 @@

/**
* get the reference, not a clone, of the style object for a given character,
* if not style is set for a pre det
* Get a reference, not a clone, to the style object for a given character,
* if no style is set for a line or char, return a new empty object.
* This is tricky and confusing because when you get an empty object you can't
* determine if it is a reference or a new one.
* @TODO this should always return a reference or always a clone or undefined when necessary.
* @protected
* @param {Number} lineIndex
* @param {Number} charIndex
* @return {Object} style object a REFERENCE to the existing one or a new empty object
* @return {TextStyleDeclaration} a style object reference to the existing one or a new empty object when undefined
*/

@@ -256,0 +268,0 @@ _getStyleDeclaration(

@@ -8,3 +8,3 @@ import { config } from '../../config';

import { FabricObjectSVGExportMixin } from '../Object/FabricObjectSVGExportMixin';
import type { TextStyleDeclaration } from './StyledText';
import { type TextStyleDeclaration } from './StyledText';
import { JUSTIFY } from '../Text/constants';

@@ -11,0 +11,0 @@ import type { FabricText } from './Text';

@@ -228,5 +228,6 @@ import type { TClassProperties, TOptions } from '../typedefs';

/**
* @protected
* @param {Number} lineIndex
* @param {Number} charIndex
* @private
* @return {TextStyleDeclaration} a style object reference to the existing one or a new empty object when undefined
*/

@@ -260,6 +261,3 @@ _getStyleDeclaration(

const map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
this.styles[lineIndex][charIndex] = style;
super._setStyleDeclaration(map.line, map.offset + charIndex, style);
}

@@ -272,7 +270,5 @@

*/
_deleteStyleDeclaration(lineIndex: number, charIndex: number) {
protected _deleteStyleDeclaration(lineIndex: number, charIndex: number) {
const map = this._styleMap[lineIndex];
lineIndex = map.line;
charIndex = map.offset + charIndex;
delete this.styles[lineIndex][charIndex];
super._deleteStyleDeclaration(map.line, map.offset + charIndex);
}

@@ -301,3 +297,3 @@

const map = this._styleMap[lineIndex];
this.styles[map.line] = {};
super._setLineStyle(map.line);
}

@@ -304,0 +300,0 @@

import { noop } from '../../constants';
import type { Pattern } from '../../Pattern';
import type { FabricObject } from '../../shapes/Object/FabricObject';
import type { Abortable, TCrossOrigin, TFiller } from '../../typedefs';
import type {
Abortable,
Constructor,
TCrossOrigin,
TFiller,
} from '../../typedefs';
import { createImage } from './dom';

@@ -10,2 +15,3 @@ import { classRegistry } from '../../ClassRegistry';

import { FabricError, SignalAbortedError } from '../internals/console';
import type { Gradient } from '../../gradient';

@@ -92,9 +98,10 @@ export type LoadImageOptions = Abortable & {

classRegistry
.getClass(obj.type)
.fromObject(obj, {
signal,
reviver,
})
.then((fabricInstance: T) => {
reviver<T>(obj, fabricInstance);
.getClass<
Constructor<T> & {
fromObject(options: any, context: Abortable): Promise<T>;
}
>(obj.type)
.fromObject(obj, { signal })
.then((fabricInstance) => {
reviver(obj, fabricInstance);
instances.push(fabricInstance);

@@ -142,3 +149,3 @@ return fabricInstance;

if (value.colorStops) {
return new (classRegistry.getClass('gradient'))(value);
return new (classRegistry.getClass<typeof Gradient>('gradient'))(value);
}

@@ -157,3 +164,3 @@ // clipPath

return classRegistry
.getClass('pattern')
.getClass<typeof Pattern>('pattern')
.fromObject(value, { signal })

@@ -160,0 +167,0 @@ .then((pattern: Pattern) => {

@@ -7,2 +7,3 @@ import {

isFiller,
isActiveSelection,
} from './typeAssertions';

@@ -16,2 +17,5 @@ import { FabricText } from '../shapes/Text/Text';

import { Shadow } from '../Shadow';
import { ActiveSelection } from '../shapes/ActiveSelection';
import { Canvas } from '../canvas/Canvas';
import { Group } from '../shapes/Group';

@@ -111,2 +115,14 @@ describe('typeAssertions', () => {

});
describe('isActiveSelection', () => {
test('can detect activeSelection', () => {
const as = new ActiveSelection([], {
canvas: new Canvas(),
});
expect(isActiveSelection(as)).toBe(true);
});
test('can safeguard against a group', () => {
const group = new Group([]);
expect(isActiveSelection(group)).toBe(false);
});
});
});

@@ -6,2 +6,3 @@ import type { FabricObject } from '../shapes/Object/Object';

import type { Path } from '../shapes/Path';
import type { ActiveSelection } from '../shapes/ActiveSelection';

@@ -45,1 +46,6 @@ export const isFiller = (

};
export const isActiveSelection = (
fabricObject?: FabricObject
): fabricObject is ActiveSelection =>
!!fabricObject && Object.hasOwn(fabricObject, 'multiSelectionStacking');

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc