Skip to content

Commit

Permalink
feat(drag-drop): add predicate function for whether an item can be so…
Browse files Browse the repository at this point in the history
…rted into an index (#20389)

Adds the `cdkDropListSortPredicate` input that allows consumers to pass in a predicate function that determines whether an item can be sorted into a particular index of the list.

Fixes #19436.
  • Loading branch information
crisbeto committed Sep 16, 2020
1 parent 2b1d84e commit d14d986
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 4 deletions.
56 changes: 56 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Expand Up @@ -4153,6 +4153,62 @@ describe('CdkDrag', () => {
expect(placeholder).toBeTruthy();
}));

it('should not move item into position not allowed by the sort predicate', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const dragItems = fixture.componentInstance.dragItems;
const spy = jasmine.createSpy('sort predicate spy').and.returnValue(false);
fixture.componentInstance.dropInstance.sortPredicate = spy;

expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
.toEqual(['Zero', 'One', 'Two', 'Three']);

const firstItem = dragItems.first;
const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect();

startDraggingViaMouse(fixture, firstItem.element.nativeElement);
dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1);
fixture.detectChanges();

expect(spy).toHaveBeenCalledWith(2, firstItem, fixture.componentInstance.dropInstance);
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
.toEqual(['Zero', 'One', 'Two', 'Three']);

dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();

const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];

// Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will
// go into an infinite loop trying to stringify the event, if the test fails.
expect(event).toEqual({
previousIndex: 0,
currentIndex: 0,
item: firstItem,
container: fixture.componentInstance.dropInstance,
previousContainer: fixture.componentInstance.dropInstance,
isPointerOverContainer: jasmine.any(Boolean),
distance: {x: jasmine.any(Number), y: jasmine.any(Number)}
});
}));

it('should not call the sort predicate for the same index', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const spy = jasmine.createSpy('sort predicate spy').and.returnValue(true);
fixture.componentInstance.dropInstance.sortPredicate = spy;

const item = fixture.componentInstance.dragItems.first.element.nativeElement;
const itemRect = item.getBoundingClientRect();

startDraggingViaMouse(fixture, item);
dispatchMouseEvent(document, 'mousemove', itemRect.left + 10, itemRect.top + 10);
fixture.detectChanges();

expect(spy).not.toHaveBeenCalled();
}));

});

describe('in a connected drop container', () => {
Expand Down
9 changes: 9 additions & 0 deletions src/cdk/drag-drop/directives/drop-list.ts
Expand Up @@ -127,6 +127,10 @@ export class CdkDropList<T = any> implements OnDestroy {
@Input('cdkDropListEnterPredicate')
enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = () => true

/** Functions that is used to determine whether an item can be sorted into a particular index. */
@Input('cdkDropListSortPredicate')
sortPredicate: (index: number, drag: CdkDrag, drop: CdkDropList) => boolean = () => true

/** Whether to auto-scroll the view when the user moves their pointer close to the edges. */
@Input('cdkDropListAutoScrollDisabled')
autoScrollDisabled: boolean;
Expand Down Expand Up @@ -185,6 +189,11 @@ export class CdkDropList<T = any> implements OnDestroy {
return this.enterPredicate(drag.data, drop.data);
};

this._dropListRef.sortPredicate =
(index: number, drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
return this.sortPredicate(index, drag.data, drop.data);
};

this._setupInputSyncSubscription(this._dropListRef);
this._handleEvents(this._dropListRef);
CdkDropList._dropLists.push(this);
Expand Down
8 changes: 8 additions & 0 deletions src/cdk/drag-drop/drag-drop.md
Expand Up @@ -211,3 +211,11 @@ moved by a user. The element's position can be explicitly set, however, via the
draggable's position after a user has navigated away and then returned.

<!-- example(cdk-drag-drop-free-drag-position) -->

### Controlling whether an item can be sorted into a particular index
`cdkDrag` items can be sorted into any position inside of a `cdkDropList` by default. You can change
this behavior by setting a `cdkDropListSortPredicate`. The predicate function will be called
whenever an item is about to be moved into a new index. If the predicate returns `true`, the
item will be moved into the new index, otherwise it will keep its current position.

<!-- example(cdk-drag-drop-sort-predicate) -->
10 changes: 7 additions & 3 deletions src/cdk/drag-drop/drop-list-ref.ts
Expand Up @@ -97,6 +97,9 @@ export class DropListRef<T = any> {
*/
enterPredicate: (drag: DragRef, drop: DropListRef) => boolean = () => true;

/** Functions that is used to determine whether an item can be sorted into a particular index. */
sortPredicate: (index: number, drag: DragRef, drop: DropListRef) => boolean = () => true;

/** Emits right before dragging has started. */
beforeStarted = new Subject<void>();

Expand Down Expand Up @@ -739,10 +742,9 @@ export class DropListRef<T = any> {
* @param delta Direction in which the user is moving their pointer.
*/
private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number,
delta?: {x: number, y: number}) {
delta?: {x: number, y: number}): number {
const isHorizontal = this._orientation === 'horizontal';

return findIndex(this._itemPositions, ({drag, clientRect}, _, array) => {
const index = findIndex(this._itemPositions, ({drag, clientRect}, _, array) => {
if (drag === item) {
// If there's only one item left in the container, it must be
// the dragged item itself so we use it as a reference.
Expand All @@ -767,6 +769,8 @@ export class DropListRef<T = any> {
pointerX >= Math.floor(clientRect.left) && pointerX < Math.floor(clientRect.right) :
pointerY >= Math.floor(clientRect.top) && pointerY < Math.floor(clientRect.bottom);
});

return (index === -1 || !this.sortPredicate(index, item, this)) ? -1 : index;
}

/** Caches the current items in the list and their positions. */
Expand Down
@@ -0,0 +1,48 @@
.example-list {
border: solid 1px #ccc;
min-height: 60px;
background: white;
border-radius: 4px;
overflow: hidden;
display: block;
width: 400px;
max-width: 100%;
}

.example-box {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
color: rgba(0, 0, 0, 0.87);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: move;
background: white;
font-size: 14px;
}

.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.cdk-drag-placeholder {
opacity: 0;
}

.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

.example-box:last-child {
border: none;
}

.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
@@ -0,0 +1,11 @@
<div
cdkDropList
class="example-list"
(cdkDropListDropped)="drop($event)"
[cdkDropListSortPredicate]="sortPredicate">
<div
class="example-box"
*ngFor="let number of numbers"
[cdkDragData]="number"
cdkDrag>{{number}}</div>
</div>
@@ -0,0 +1,26 @@
import {Component} from '@angular/core';
import {CdkDragDrop, moveItemInArray, CdkDrag} from '@angular/cdk/drag-drop';

/**
* @title Drag&Drop sort predicate
*/
@Component({
selector: 'cdk-drag-drop-sort-predicate-example',
templateUrl: 'cdk-drag-drop-sort-predicate-example.html',
styleUrls: ['cdk-drag-drop-sort-predicate-example.css'],
})
export class CdkDragDropSortPredicateExample {
numbers = [1, 2, 3, 4, 5, 6, 7, 8];

drop(event: CdkDragDrop<unknown>) {
moveItemInArray(this.numbers, event.previousIndex, event.currentIndex);
}

/**
* Predicate function that only allows even numbers to be
* sorted into even indices and odd numbers at odd indices.
*/
sortPredicate(index: number, item: CdkDrag<number>) {
return (index + 1) % 2 === item.data % 2;
}
}
5 changes: 5 additions & 0 deletions src/components-examples/cdk/drag-drop/index.ts
Expand Up @@ -38,6 +38,9 @@ import {
CdkDragDropRootElementExample
} from './cdk-drag-drop-root-element/cdk-drag-drop-root-element-example';
import {CdkDragDropSortingExample} from './cdk-drag-drop-sorting/cdk-drag-drop-sorting-example';
import {
CdkDragDropSortPredicateExample
} from './cdk-drag-drop-sort-predicate/cdk-drag-drop-sort-predicate-example';

export {
CdkDragDropAxisLockExample,
Expand All @@ -56,6 +59,7 @@ export {
CdkDragDropOverviewExample,
CdkDragDropRootElementExample,
CdkDragDropSortingExample,
CdkDragDropSortPredicateExample,
};

const EXAMPLES = [
Expand All @@ -75,6 +79,7 @@ const EXAMPLES = [
CdkDragDropOverviewExample,
CdkDragDropRootElementExample,
CdkDragDropSortingExample,
CdkDragDropSortPredicateExample,
];

@NgModule({
Expand Down
4 changes: 3 additions & 1 deletion tools/public_api_guard/cdk/drag-drop.d.ts
Expand Up @@ -168,6 +168,7 @@ export declare class CdkDropList<T = any> implements OnDestroy {
id: string;
lockAxis: DragAxis;
orientation: DropListOrientation;
sortPredicate: (index: number, drag: CdkDrag, drop: CdkDropList) => boolean;
sorted: EventEmitter<CdkDragSortEvent<T>>;
sortingDisabled: boolean;
constructor(
Expand All @@ -180,7 +181,7 @@ export declare class CdkDropList<T = any> implements OnDestroy {
static ngAcceptInputType_autoScrollDisabled: BooleanInput;
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_sortingDisabled: BooleanInput;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDropList<any>, "[cdkDropList], cdk-drop-list", ["cdkDropList"], { "connectedTo": "cdkDropListConnectedTo"; "data": "cdkDropListData"; "orientation": "cdkDropListOrientation"; "id": "id"; "lockAxis": "cdkDropListLockAxis"; "disabled": "cdkDropListDisabled"; "sortingDisabled": "cdkDropListSortingDisabled"; "enterPredicate": "cdkDropListEnterPredicate"; "autoScrollDisabled": "cdkDropListAutoScrollDisabled"; }, { "dropped": "cdkDropListDropped"; "entered": "cdkDropListEntered"; "exited": "cdkDropListExited"; "sorted": "cdkDropListSorted"; }, never>;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDropList<any>, "[cdkDropList], cdk-drop-list", ["cdkDropList"], { "connectedTo": "cdkDropListConnectedTo"; "data": "cdkDropListData"; "orientation": "cdkDropListOrientation"; "id": "id"; "lockAxis": "cdkDropListLockAxis"; "disabled": "cdkDropListDisabled"; "sortingDisabled": "cdkDropListSortingDisabled"; "enterPredicate": "cdkDropListEnterPredicate"; "sortPredicate": "cdkDropListSortPredicate"; "autoScrollDisabled": "cdkDropListAutoScrollDisabled"; }, { "dropped": "cdkDropListDropped"; "entered": "cdkDropListEntered"; "exited": "cdkDropListExited"; "sorted": "cdkDropListSorted"; }, never>;
static ɵfac: i0.ɵɵFactoryDef<CdkDropList<any>, [null, null, null, { optional: true; }, { optional: true; skipSelf: true; }, null, { optional: true; }]>;
}

Expand Down Expand Up @@ -357,6 +358,7 @@ export declare class DropListRef<T = any> {
container: DropListRef;
}>;
lockAxis: 'x' | 'y';
sortPredicate: (index: number, drag: DragRef, drop: DropListRef) => boolean;
sorted: Subject<{
previousIndex: number;
currentIndex: number;
Expand Down

0 comments on commit d14d986

Please sign in to comment.