fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
255 lines (231 loc) • 7.93 kB
text/typescript
import type { ControlRenderingStyleOverride } from '../controls/controlRendering';
import { classRegistry } from '../ClassRegistry';
import type { GroupProps } from './Group';
import { Group } from './Group';
import type { FabricObject } from './Object/FabricObject';
import {
LAYOUT_TYPE_ADDED,
LAYOUT_TYPE_REMOVED,
} from '../LayoutManager/constants';
import type { TClassProperties } from '../typedefs';
import { log } from '../util/internals/console';
import { ActiveSelectionLayoutManager } from '../LayoutManager/ActiveSelectionLayoutManager';
export type MultiSelectionStacking = 'canvas-stacking' | 'selection-order';
export interface ActiveSelectionOptions extends GroupProps {
multiSelectionStacking: MultiSelectionStacking;
}
const activeSelectionDefaultValues: Partial<TClassProperties<ActiveSelection>> =
{
multiSelectionStacking: 'canvas-stacking',
};
/**
* Used by Canvas to manage selection.
*
* @example
* class MyActiveSelection extends ActiveSelection {
* ...
* }
*
* // override the default `ActiveSelection` class
* classRegistry.setClass(MyActiveSelection)
*/
export class ActiveSelection extends Group {
static type = 'ActiveSelection';
static ownDefaults: Record<string, any> = activeSelectionDefaultValues;
static getDefaults(): Record<string, any> {
return { ...super.getDefaults(), ...ActiveSelection.ownDefaults };
}
/**
* The ActiveSelection needs to use the ActiveSelectionLayoutManager
* or selections on interactive groups may be broken
*/
declare layoutManager: ActiveSelectionLayoutManager;
/**
* controls how selected objects are added during a multiselection event
* - `canvas-stacking` adds the selected object to the active selection while respecting canvas object stacking order
* - `selection-order` adds the selected object to the top of the stack,
* meaning that the stack is ordered by the order in which objects were selected
* @default `canvas-stacking`
*/
declare multiSelectionStacking: MultiSelectionStacking;
constructor(
objects: FabricObject[] = [],
options: Partial<ActiveSelectionOptions> = {},
) {
super();
Object.assign(this, ActiveSelection.ownDefaults);
this.setOptions(options);
const { left, top, layoutManager } = options;
this.groupInit(objects, {
left,
top,
layoutManager: layoutManager ?? new ActiveSelectionLayoutManager(),
});
}
/**
* @private
*/
_shouldSetNestedCoords() {
return true;
}
/**
* @private
* @override we don't want the selection monitor to be active
*/
__objectSelectionMonitor() {
// noop
}
/**
* Adds objects with respect to {@link multiSelectionStacking}
* @param targets object to add to selection
*/
multiSelectAdd(...targets: FabricObject[]) {
if (this.multiSelectionStacking === 'selection-order') {
this.add(...targets);
} else {
// respect object stacking as it is on canvas
// perf enhancement for large ActiveSelection: consider a binary search of `isInFrontOf`
targets.forEach((target) => {
const index = this._objects.findIndex((obj) => obj.isInFrontOf(target));
const insertAt =
index === -1
? // `target` is in front of all other objects
this.size()
: index;
this.insertAt(insertAt, target);
});
}
}
/**
* @override block ancestors/descendants of selected objects from being selected to prevent a circular object tree
*/
canEnterGroup(object: FabricObject) {
if (
this.getObjects().some(
(o) => o.isDescendantOf(object) || object.isDescendantOf(o),
)
) {
// prevent circular object tree
log(
'error',
'ActiveSelection: circular object trees are not supported, this call has no effect',
);
return false;
}
return super.canEnterGroup(object);
}
/**
* Change an object so that it can be part of an active selection.
* this method is called by multiselectAdd from canvas code.
* @private
* @param {FabricObject} object
* @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane
*/
enterGroup(object: FabricObject, removeParentTransform?: boolean) {
// This condition check that the object has currently a group, and the group
// is also its parent, meaning that is not in an active selection, but is
// in a normal group.
if (object.parent && object.parent === object.group) {
// Disconnect the object from the group functionalities, but keep the ref parent intact
// for later re-enter
object.parent._exitGroup(object);
// in this case the object is probably inside an active selection.
} else if (object.group && object.parent !== object.group) {
// in this case group.remove will also clear the old parent reference.
object.group.remove(object);
}
// enter the active selection from a render perspective
// the object will be in the objects array of both the ActiveSelection and the Group
// but referenced in the group's _activeObjects so that it won't be rendered twice.
this._enterGroup(object, removeParentTransform);
}
/**
* we want objects to retain their canvas ref when exiting instance
* @private
* @param {FabricObject} object
* @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it
*/
exitGroup(object: FabricObject, removeParentTransform?: boolean) {
this._exitGroup(object, removeParentTransform);
// return to parent
object.parent && object.parent._enterGroup(object, true);
}
/**
* @private
* @param {'added'|'removed'} type
* @param {FabricObject[]} targets
*/
_onAfterObjectsChange(type: 'added' | 'removed', targets: FabricObject[]) {
super._onAfterObjectsChange(type, targets);
const groups = new Set<Group>();
targets.forEach((object) => {
const { parent } = object;
parent && groups.add(parent);
});
if (type === LAYOUT_TYPE_REMOVED) {
// invalidate groups' layout and mark as dirty
groups.forEach((group) => {
group._onAfterObjectsChange(LAYOUT_TYPE_ADDED, targets);
});
} else {
// mark groups as dirty
groups.forEach((group) => {
group._set('dirty', true);
});
}
}
/**
* @override remove all objects
*/
onDeselect() {
this.removeAll();
return false;
}
/**
* Returns string representation of a group
* @return {String}
*/
toString() {
return `#<ActiveSelection: (${this.complexity()})>`;
}
/**
* Decide if the object should cache or not. The Active selection never caches
* @return {Boolean}
*/
shouldCache() {
return false;
}
/**
* Check if this group or its parent group are caching, recursively up
* @return {Boolean}
*/
isOnACache() {
return false;
}
/**
* Renders controls and borders for the object
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Object} [styleOverride] properties to override the object style
* @param {Object} [childrenOverride] properties to override the children overrides
*/
_renderControls(
ctx: CanvasRenderingContext2D,
styleOverride?: ControlRenderingStyleOverride,
childrenOverride?: ControlRenderingStyleOverride,
) {
ctx.save();
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
const options = {
hasControls: false,
...childrenOverride,
forActiveSelection: true,
};
for (let i = 0; i < this._objects.length; i++) {
this._objects[i]._renderControls(ctx, options);
}
super._renderControls(ctx, styleOverride);
ctx.restore();
}
}
classRegistry.setClass(ActiveSelection);
classRegistry.setClass(ActiveSelection, 'activeSelection');