Skip to content

Commit

Permalink
register AS on group
Browse files Browse the repository at this point in the history
  • Loading branch information
ShaMan123 committed Mar 24, 2024
1 parent 6b26a1a commit 6aa468c
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 80 deletions.
70 changes: 25 additions & 45 deletions src/LayoutManager/ActiveSelectionLayoutManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ describe('ActiveSelectionLayoutManager', () => {
'performLayout'
);
groupPerformLayout.mockClear();
groupPerformLayout2.mockClear();
asPerformLayout.mockClear();
expect(group.layoutManager['_subscriptions'].get(as)).toBeDefined();
expect(group2.layoutManager['_subscriptions'].get(as)).toBeDefined();
expect(manager['_subscriptions'].size).toBe(0);

const event = { foo: 'bar' };
triggers.forEach((trigger) => as.fire(trigger, event));
expect(asPerformLayout).not.toHaveBeenCalled();
Expand All @@ -88,46 +93,14 @@ describe('ActiveSelectionLayoutManager', () => {
},
]),
]);
groupPerformLayout.mockClear();
asPerformLayout.mockClear();
// we don't keep record of subscriptions on objects
expect(manager['_subscriptions'].get(object)).toBeUndefined();
expect(manager['_subscriptions'].get(object2)).toBeUndefined();
expect(manager['_subscriptions'].get(object3)).toBeUndefined();
expect(manager['_subscriptions'].get(object4)).toBeUndefined();
expect(manager['_subscriptions'].get(group2)).toBeDefined();
expect(manager['_subscriptions'].get(group)).toBeDefined();
manager.unsubscribeTargets({
targets: [object, object2, object3, object4],
target: as,
});
expect(manager['_subscriptions'].get(group2)).toBeUndefined();
expect(manager['_subscriptions'].get(group)).toBeUndefined();
triggers.forEach((trigger) => as.fire(trigger, event));
expect(groupPerformLayout).not.toHaveBeenCalled();
expect(asPerformLayout).not.toHaveBeenCalled();
});
expect(groupPerformLayout).toHaveBeenCalledTimes(triggers.length);
expect(groupPerformLayout2).toHaveBeenCalledTimes(triggers.length);

it('a subscribed activeSelection with more objects in the same parent has a bug', () => {
const manager = new ActiveSelectionLayoutManager();
const object = new FabricObject();
const object2 = new FabricObject();
const object3 = new FabricObject();
const group = new Group([object, object2, object3], {
interactive: true,
subTargetCheck: true,
});
const as = new ActiveSelection([object, object2, object3], {
layoutManager: manager,
});
const asPerformLayout = jest.spyOn(manager, 'performLayout');
const groupPerformLayout = jest.spyOn(
group.layoutManager,
'performLayout'
);
as.remove(object);
groupPerformLayout.mockClear();
groupPerformLayout2.mockClear();
asPerformLayout.mockClear();
const event = { foo: 'bar' };

triggers.forEach((trigger) => as.fire(trigger, event));
expect(asPerformLayout).not.toHaveBeenCalled();
expect(groupPerformLayout.mock.calls).toMatchObject([
Expand All @@ -148,20 +121,27 @@ describe('ActiveSelectionLayoutManager', () => {
},
]),
]);
expect(groupPerformLayout).toHaveBeenCalledTimes(triggers.length);
expect(groupPerformLayout2).toHaveBeenCalledTimes(triggers.length);

groupPerformLayout.mockClear();
groupPerformLayout2.mockClear();
asPerformLayout.mockClear();
// we don't keep record of subscriptions on objects
expect(manager['_subscriptions'].get(object)).toBeUndefined();
expect(manager['_subscriptions'].get(object2)).toBeUndefined();
expect(manager['_subscriptions'].get(object3)).toBeUndefined();
expect(manager['_subscriptions'].get(group)).toBeDefined();
as.remove(object3);

as.remove(object2);
expect(group.layoutManager['_subscriptions'].get(as)).toBeUndefined();
expect(group2.layoutManager['_subscriptions'].get(as)).toBeDefined();
as.removeAll();
expect(group2.layoutManager['_subscriptions'].get(as)).toBeUndefined();

groupPerformLayout.mockClear();
groupPerformLayout2.mockClear();
asPerformLayout.mockClear();
// BUG! i removed an object only and group layour is not called anymore
expect(manager['_subscriptions'].get(group)).toBeUndefined();

triggers.forEach((trigger) => as.fire(trigger, event));
expect(groupPerformLayout).not.toHaveBeenCalled();
expect(groupPerformLayout2).not.toHaveBeenCalled();
expect(asPerformLayout).not.toHaveBeenCalled();
});
});
});
Expand Down
68 changes: 33 additions & 35 deletions src/LayoutManager/ActiveSelectionLayoutManager.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,54 @@
import { LayoutManager } from './LayoutManager';
import type { RegistrationContext, StrictLayoutContext } from './types';
import type { FabricObject } from '../shapes/Object/FabricObject';
import type { Group } from '../shapes/Group';

/**
* Today the LayoutManager class also takes care of subscribing event handlers
* to update the group layout when the group is interactive and a transform is applied
* to a child object.
* The ActiveSelection is never interactive, but it could contain obejcts from
* The ActiveSelection is never interactive, but it could contain objects from
* groups that are.
* The standard LayoutManager would subscribe the children of the activeSelection to
* perform layout changes to the activeselection itself, what we need instead is that
* perform layout changes to the active selection itself, what we need instead is that
* the transformation applied to the active selection will trigger changes to the
* original group of the children ( the one referenced under the parent property )
* This subclass of the LayoutManager has a single duty to fill the gap of this difference.
* This subclass of the LayoutManager has a single duty to fill the gap of this difference.`
*/
export class ActiveSelectionLayoutManager extends LayoutManager {
/**
* Subscribe an object to transform events that will trigger a layout change on the parent
* This is important only for interactive groups.
* @param object
* @param context
*/
protected subscribe(
object: FabricObject,
subscribeTargets(
context: RegistrationContext & Partial<StrictLayoutContext>
) {
const { parent, group: activeSelection } = object;
if (!parent || !activeSelection || activeSelection === parent) {
// nothing to do here
return;
}
this.unsubscribe(object, context);
const disposers = this.attachHandlers(activeSelection, {
target: parent,
targets: context.targets,
): void {
const activeSelection = context.target;
const parents = context.targets.reduce((parents, target) => {
target.parent && parents.add(target.parent);
return parents;
}, new Set<Group>());
parents.forEach((parent) => {
parent.layoutManager.subscribeTargets({
target: parent,
targets: [activeSelection],
});
});
// the _subscriptions Map is using the parent as a key.
// This will ensure that a single set of events is kept for each unique parent
this._subscriptions.set(parent, disposers);
}

/**
* unsubscribe object layout triggers
* unsubscribe from parent only if all its children were deselected
*/
protected unsubscribe(
object: FabricObject,
context?: RegistrationContext & Partial<StrictLayoutContext>
) {
const { parent } = object;
if (parent) {
(this._subscriptions.get(parent) || []).forEach((d) => d());
this._subscriptions.delete(parent);
}
unsubscribeTargets(
context: RegistrationContext & Partial<StrictLayoutContext>
): void {
const activeSelection = context.target;
const selectedObjects = activeSelection.getObjects();
const parents = context.targets.reduce((parents, target) => {
target.parent && parents.add(target.parent);
return parents;
}, new Set<Group>());
parents.forEach((parent) => {
!selectedObjects.some((object) => object.parent === parent) &&
parent.layoutManager.unsubscribeTargets({
target: parent,
targets: [activeSelection],
});
});
}
}

0 comments on commit 6aa468c

Please sign in to comment.