Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platform, cdk): Drag & drop auto detect mode #9626

Merged
merged 3 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"docs-cdk-clicked": "libs/docs/cdk/clicked",
"docs-cdk-data-source": "libs/docs/cdk/data-source",
"docs-cdk-disabled": "libs/docs/cdk/disabled",
"docs-cdk-dnd": "libs/docs/cdk/drag-n-drop",
"docs-cdk-focusable-item": "libs/docs/cdk/focusable-item",
"docs-cdk-focusable-list": "libs/docs/cdk/focusable-list",
"docs-cdk-forms": "libs/docs/cdk/forms",
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/app/cdk/cdk-documentation.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const ROUTES: Routes = [
path: 'data-source',
loadChildren: () => import('@fundamental-ngx/docs/cdk/data-source').then((m) => m.DataSourceDocsModule)
},
{
path: 'drag-n-drop',
loadChildren: () => import('@fundamental-ngx/docs/cdk/drag-n-drop').then((m) => m.DndDocsModule)
},
{
path: 'focusable-item',
loadChildren: () =>
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/app/cdk/documentation/cdk-documentation-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const directives: SectionInterfaceContent[] = [
url: 'cdk/data-source',
name: 'Data Source'
},
{
url: 'cdk/drag-n-drop',
name: 'Drag&Drop'
},
{
url: 'cdk/focusable-list',
name: 'Focusable List'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,18 @@ export const components: SectionInterfaceContent[] = [
{ url: 'platform/search-field', name: 'Search Field' },
{ url: 'platform/smart-filter-bar', name: 'Smart Filter Bar' },
{ url: 'platform/split-menu-button', name: 'Split Menu Button' },
{ url: 'platform/table', name: 'Table' },
{
url: 'platform/table',
name: 'Table',
subItems: [
{ url: 'platform/table/basic', name: 'Basic examples' },
{ url: 'platform/table/p13-dialog-table', name: 'Personalization dialog' },
{ url: 'platform/table/settings-dialog-table', name: 'Settings dialog' },
{ url: 'platform/table/row-selection', name: 'Row selection' },
{ url: 'platform/table/scrolling', name: 'Scrolling options' },
{ url: 'platform/table/clickable-rows', name: 'Clickable rows' }
]
},
{ url: 'platform/textarea', name: 'Textarea' },
{ url: 'platform/thumbnail', name: 'Thumbnail' },
{ url: 'platform/time-picker', name: 'Time Picker' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,14 @@ describe('DndListDirective', () => {
directive.items = [...component.list];

(directive as any)._closestItemIndex = 1;
(directive as any)._closestItemPosition = 'after';
directive.dragEnd(3);
expect(directive.itemDropped.emit).toHaveBeenCalledWith({
replacedItemIndex: 1,
draggedItemIndex: 3,
items: ['item1', 'item4', 'item2', 'item3']
items: ['item1', 'item4', 'item2', 'item3'],
insertAt: 'after',
mode: 'shift'
});

expect((directive as any)._removeAllLines).toHaveBeenCalled();
Expand Down
161 changes: 151 additions & 10 deletions libs/cdk/src/lib/utils/drag-and-drop/dnd-list/dnd-list.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
ElementRef,
EventEmitter,
forwardRef,
HostBinding,
Input,
OnDestroy,
Output,
QueryList
} from '@angular/core';
import { merge, Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { ElementChord, FdDropEvent, LinkPosition, ElementPosition, DndItem } from '../dnd.interfaces';
import { ElementChord, FdDropEvent, LinkPosition, ElementPosition, DndItem, FdDndDropType } from '../dnd.interfaces';
import { DND_ITEM, DND_LIST } from '../tokens';

@Directive({
Expand All @@ -21,15 +22,51 @@ import { DND_ITEM, DND_LIST } from '../tokens';
})
export class DndListDirective<T> implements AfterContentInit, OnDestroy {
/**
* Defines if the the element is allowed to be dragged in 2 dimensions,
* Defines if the element is allowed to be dragged in 2 dimensions,
* When true - replace indicator will be displayed vertically
*/
@Input()
gridMode = false;

/** When enabled, replace indicator will appear on whole element, instead of horizontal/vertical line before/after element */
/**
* Defines drop strategy:
* * `shift` mode will create line after closest drop element.
* * `group` mode will create replace indicator on whole closest drop element.
* * `auto` mode will create line after closest drop element,
* if dragged element coordinates are shifted for 30% from center of the closest drop element.
* Otherwise, it will create replace indicator on whole closest drop element.
*
* `shift` mode is the default.
*/
@Input()
replaceMode = false;
dropMode: FdDndDropType = 'shift';

/**
* Threshold of dragged item over another item to define which type of `dropMode` should be used.
*/
@Input()
threshold = 0.3;

/**
* @deprecated
* Use `dropMode` property for better configuration.
*
* @description
* When enabled, replace indicator will appear on whole element, instead of horizontal/vertical line before/after element.
*/
@Input()
set replaceMode(value: boolean) {
this._replaceMode = value;
this.dropMode = value ? 'group' : 'shift';
this._detectedDropMode = this.dropMode;
}

get replaceMode(): boolean {
return this._replaceMode;
}

/** @hidden */
private _replaceMode = false;

/** Array of items, that will be sorted */
@Input()
Expand Down Expand Up @@ -75,6 +112,19 @@ export class DndListDirective<T> implements AfterContentInit, OnDestroy {
/** @hidden */
private _draggable = true;

/** @hidden */
@HostBinding('class')
private readonly _initialClass = 'fd-dnd-list';

/** @hidden */
private _detectedDropMode: 'shift' | 'group';

/** @hidden */
private _linesRemoved = true;

/** @hidden */
private _indicatorsRemoved = true;

/** @hidden */
ngAfterContentInit(): void {
this._changeDraggableState(this._draggable);
Expand Down Expand Up @@ -109,8 +159,13 @@ export class DndListDirective<T> implements AfterContentInit, OnDestroy {
closestItemIndex = null;
}

/** If the closest element is different than the old one, new one is picked. It prevents from performance issues */
if ((closestItemIndex || closestItemIndex === 0) && closestItemIndex !== this._closestItemIndex) {
/** If the closest element is different from the old one, new one is picked. It prevents from performance issues */
if (
(closestItemIndex || closestItemIndex === 0) &&
(closestItemIndex !== this._closestItemIndex || this.dropMode === 'auto')
) {
this._removeAllLines();
this._removeAllReplaceIndicators();
this._closestItemIndex = closestItemIndex;
this._closestItemPosition = this._elementsCoordinates[closestItemIndex].position;
// If closest item index is same as dragged item, just remove indicators
Expand All @@ -120,18 +175,20 @@ export class DndListDirective<T> implements AfterContentInit, OnDestroy {
return;
}
/** Generating line, that shows where the element will be placed, on drop */
if (this.replaceMode) {
if (this.dropMode === 'group') {
this._createReplaceIndicator(this._closestItemIndex);
} else {
} else if (this.dropMode === 'shift') {
this._createLine(this._closestItemIndex, this._closestItemPosition);
} else {
this._selectDropModeIndicator(draggedItemIndex, closestItem, closestItemIndex);
}
}
}

/** Method called, when element is started to be dragged */
dragStart(index: number): void {
const draggedItemElement = this._dndItemReference[index].elementRef;
/** Counting all of the elements's chords */
/** Counting all the element's chords */
this._elementsCoordinates = this._dndItemReference.map((item: DndItem) =>
item.getElementCoordinates(this._isBefore(draggedItemElement, item.elementRef), this.gridMode)
);
Expand Down Expand Up @@ -164,7 +221,9 @@ export class DndListDirective<T> implements AfterContentInit, OnDestroy {
this.itemDropped.emit({
replacedItemIndex,
draggedItemIndex,
items
items,
insertAt: this._closestItemPosition,
mode: this.dropMode !== 'auto' ? this.dropMode : this._detectedDropMode
});

this._removeAllLines();
Expand All @@ -177,25 +236,70 @@ export class DndListDirective<T> implements AfterContentInit, OnDestroy {
}
}

/** @hidden */
private _selectDropModeIndicator(
draggedItemIndex: number,
closestItem: ElementChord | undefined,
closestItemIndex: number
): void {
if (!closestItem || !this._dndItemReference[draggedItemIndex]) {
return;
}

let newDetectedDropMode: 'shift' | 'group';
const draggedElmCoords =
this._dndItemReference[draggedItemIndex].elementRef.nativeElement.getBoundingClientRect();

const closestItemBoundaries = getElementBoundaries(closestItem, this.threshold);
const draggedItemStartCoords = getElementStartCoords(draggedElmCoords, closestItem.position);

if (
_between(draggedItemStartCoords.x, closestItemBoundaries.x.start, closestItemBoundaries.x.end) &&
_between(draggedItemStartCoords.y, closestItemBoundaries.y.start, closestItemBoundaries.y.end)
) {
newDetectedDropMode = 'group';
} else {
newDetectedDropMode = 'shift';
}

if (newDetectedDropMode === this._detectedDropMode && (!this._linesRemoved || !this._indicatorsRemoved)) {
return;
}

this._detectedDropMode = newDetectedDropMode;

if (this._detectedDropMode === 'shift') {
this._createLine(closestItemIndex, this._elementsCoordinates[closestItemIndex].position);
} else {
this._createReplaceIndicator(closestItemIndex);
}
}

/** @hidden */
private _removeAllLines(): void {
this._linesRemoved = true;
this.dndItems.forEach((item) => item.removeLine());
}

/** @hidden */
private _removeAllReplaceIndicators(): void {
this._indicatorsRemoved = true;
this.dndItems.forEach((item) => item.removeReplaceIndicator());
}

/** @hidden */
private _createLine(closestItemIndex: number, linkPosition: LinkPosition): void {
this._removeAllLines();
this._removeAllReplaceIndicators();
this._linesRemoved = false;
this._dndItemReference[closestItemIndex].createLine(linkPosition, this.gridMode);
}

/** @hidden */
private _createReplaceIndicator(closestItemIndex: number): void {
this._removeAllLines();
this._removeAllReplaceIndicators();
this._indicatorsRemoved = false;
this._dndItemReference[closestItemIndex].createReplaceIndicator();
}

Expand Down Expand Up @@ -263,3 +367,40 @@ function _isMouseOnElement(element: ElementChord, mousePosition: ElementPosition
function _between(x: number, min: number, max: number): boolean {
return x >= min && x <= max;
}

interface ElementBoundaries {
x: {
start: number;
end: number;
};
y: {
start: number;
end: number;
};
}

function getElementStartCoords(rect: DOMRect, position: ElementChord['position']): { x: number; y: number } {
return {
x: position === 'after' ? rect.x + rect.width : rect.x,
y: position === 'after' ? rect.y + rect.height : rect.y
};
}

function getElementBoundaries(coordinates: ElementChord, threshold: number): ElementBoundaries {
const widthOffset = coordinates.width * (coordinates.position === 'after' ? 1 : -1);
const heightOffset = coordinates.height * (coordinates.position === 'after' ? 1 : -1);
const xStart = coordinates.position === 'after' ? coordinates.x : coordinates.x + coordinates.width;
const xEnd = xStart + widthOffset + (widthOffset / 2) * threshold;
const yStart = coordinates.position === 'after' ? coordinates.y : coordinates.y + coordinates.height;
const yEnd = yStart + heightOffset + (heightOffset / 2) * threshold;
return {
x: {
start: xStart > xEnd ? xEnd : xStart,
end: xStart > xEnd ? xStart : xEnd
},
y: {
start: yStart > yEnd ? yEnd : yStart,
end: yStart > yEnd ? yStart : yEnd
}
};
}
5 changes: 5 additions & 0 deletions libs/cdk/src/lib/utils/drag-and-drop/dnd.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface FdDropEvent<T> {
items: Array<T>;
replacedItemIndex: number;
draggedItemIndex: number;
insertAt: 'before' | 'after' | null;
mode: FdDndDropEventMode;
}

export interface ElementPosition {
Expand All @@ -36,3 +38,6 @@ export interface DndItem {
listDraggable: boolean;
changeCDKDragState: () => void;
}

export type FdDndDropType = 'shift' | 'group' | 'auto';
export type FdDndDropEventMode = 'shift' | 'group';
4 changes: 4 additions & 0 deletions libs/cdk/src/lib/utils/drag-and-drop/drag-and-drop.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,7 @@
}
}
}

.fd-dnd-list {
position: relative;
}
32 changes: 32 additions & 0 deletions libs/docs/cdk/drag-n-drop/dnd-docs.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<fd-docs-section-title id="simple" componentName="fdkDisabled">Basic example</fd-docs-section-title>
<description>
<p>
Drag & Drop container should be decorated with <code>fdkDndList</code> directive, which accepts
<code>[items]</code> input property with the array of draggable items.
</p>
<p>Developers can define type of drop action by specifying <code>[dropMode]</code> input property:</p>
<ul>
<li>
<code>shift</code> will highlight element that's being currently hovered with dragged item with the line,
indicating that dragged item will be placed before or after the hovered element.
</li>
<li>
<code>group</code> will highlight element that's being currently hovered with dragged item by changing
element's background, indicating that dragged item will replace currently hovered element. This options is
suitable for grouping scenarios, when dropping item on another item should create a group, where dropped
item will become a child item of hovered one.
</li>
<li>
<code>auto</code> will combine both approaches described above, and will select appropriate action depending
on the coordinates of dragged item. By default it will apply <code>group</code> mode, if percentage of the
dragged element area which hovers over the hovered item is less than a <code>[threshold]</code> percentage.
Developers can specify the threshold percentage of dragged item over another item to switch between
<code>group</code> and <code>shift</code> modes.
</li>
</ul>
<p>Each draggable item element should be decorated with <code>fdkDndItem</code> directive.</p>
</description>
<component-example [hasBackground]="true">
<fundamental-ngx-cdk-disabled-example></fundamental-ngx-cdk-disabled-example>
</component-example>
<code-example [exampleFiles]="defaultExample"></code-example>
Loading