Skip to content

feat(drag-n-drop): add item dragging into grid #111

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

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
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
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,101 @@ Here is listed the basic API of both KtdGridComponent and KtdGridItemComponent.
startDragManually(startEvent: MouseEvent | TouchEvent);
```

#### KtdGridComponent
```ts
/** Type of compaction that will be applied to the layout (vertical, horizontal or free). Defaults to 'vertical' */
@Input() compactType: KtdGridCompactType = 'vertical';

/**
* Row height as number or as 'fit'.
* If rowHeight is a number value, it means that each row would have those css pixels in height.
* if rowHeight is 'fit', it means that rows will fit in the height available. If 'fit' value is set, a 'height' should be also provided.
*/
@Input() rowHeight: number | 'fit' = 100;

/** Number of columns */
@Input() cols: number = 6;

/** Layout of the grid. Array of all the grid items with its 'id' and position on the grid. */
@Input() layout: KtdGridLayout;

/** Grid gap in css pixels */
@Input() gap: number = 0;

/**
* If height is a number, fixes the height of the grid to it, recommended when rowHeight = 'fit' is used.
* If height is null, height will be automatically set according to its inner grid items.
* Defaults to null.
* */
@Input() height: number | null = null;


/**
* Parent element that contains the scroll. If an string is provided it would search that element by id on the dom.
* If no data provided or null autoscroll is not performed.
*/
@Input() scrollableParent: HTMLElement | Document | string | null = null;

/** Number of CSS pixels that would be scrolled on each 'tick' when auto scroll is performed. */
@Input() scrollSpeed: number = 2;

/** Whether or not to update the internal layout when some dependent property change. */
@Input() compactOnPropsChange = true;

/** If true, grid items won't change position when being dragged over. Handy when using no compaction */
@Input() preventCollision = false;

/** Emits when layout change */
@Output() layoutUpdated: EventEmitter<KtdGridLayout> = new EventEmitter<KtdGridLayout>();

/** Emits when drag starts */
@Output() dragStarted: EventEmitter<KtdDragStart> = new EventEmitter<KtdDragStart>();

/** Emits when resize starts */
@Output() resizeStarted: EventEmitter<KtdResizeStart> = new EventEmitter<KtdResizeStart>();

/** Emits when drag ends */
@Output() dragEnded: EventEmitter<KtdDragEnd> = new EventEmitter<KtdDragEnd>();

/** Emits when resize ends */
@Output() resizeEnded: EventEmitter<KtdResizeEnd> = new EventEmitter<KtdResizeEnd>();

/** Emits when a grid item is being resized and its bounds have changed */
@Output() gridItemResize: EventEmitter<KtdGridItemResizeEvent> = new EventEmitter<KtdGridItemResizeEvent>();

```

#### KtdDrag<T>
```ts

/** Id of the ktd drag item. This property is strictly compulsory. */
@Input() id: string;

/** Whether the item is disabled or not. Defaults to false. */
@Input() disabled: boolean = false;

/** Minimum amount of pixels that the user should move before it starts the drag sequence. */
@Input() dragStartThreshold: number = 0;

/** Whether the item is draggable or not. Defaults to true. Does not affect manual dragging using the startDragManually method. */
@Input() draggable: boolean = true;

/** Width of draggable item in number of cols. Defaults to the width of grid. */
@Input() width: number;

/** Height of draggable item in number of rows. Defaults to 1. */
@Input() height: number = 1;

/** Event emitted when the user starts dragging the item. */
@Output() dragStart: Observable<KtdDragStart>;

/** Event emitted when the user is dragging the item. !!! Emitted for every pixel. !!! */
@Output() dragMove: Observable<KtdDragStart>;

/** Event emitted when the user stops dragging the item. */
@Output() dragEnd: Observable<KtdDragStart>;

```
## TODO features

- [x] Add delete feature to Playground page.
Expand Down
208 changes: 208 additions & 0 deletions projects/angular-grid-layout/src/lib/directives/ktd-drag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {
AfterContentInit, ContentChild, ContentChildren,
Directive, ElementRef,
InjectionToken, Input, OnDestroy, Output, QueryList
} from '@angular/core';
import {coerceBooleanProperty} from "../coercion/boolean-property";
import {Observable, Observer, Subscription} from "rxjs";
import {coerceNumberProperty} from "../coercion/number-property";
import {KtdRegistryService} from "../ktd-registry.service";
import {KTD_GRID_DRAG_HANDLE, KtdGridDragHandle} from "./drag-handle";
import {DragRef} from "../utils/drag-ref";
import {KTD_GRID_ITEM_PLACEHOLDER, KtdGridItemPlaceholder} from "./placeholder";
import {KtdGridComponent, PointingDeviceEvent} from "../grid.component";
import {KtdGridService} from "../grid.service";


export const KTD_DRAG = new InjectionToken<KtdDrag<any>>('KtdDrag');

@Directive({
selector: '[ktdDrag]',
host: {
'[class.ktd-draggable]': '_dragHandles.length === 0 && draggable',
'[class.ktd-dragging]': '_dragRef.isDragging',
'[class.ktd-drag-disabled]': 'disabled',
},
providers: [{provide: KTD_DRAG, useExisting: KtdDrag}]
})
export class KtdDrag<T> implements AfterContentInit, OnDestroy {
/** Elements that can be used to drag the draggable item. */
@ContentChildren(KTD_GRID_DRAG_HANDLE, {descendants: true}) _dragHandles: QueryList<KtdGridDragHandle>;

/** Template ref for placeholder */
@ContentChild(KTD_GRID_ITEM_PLACEHOLDER) placeholder: KtdGridItemPlaceholder;

@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
this._dragRef.draggable = !this._disabled;
}
private _disabled: boolean = false;

/** Minimum amount of pixels that the user should move before it starts the drag sequence. */
@Input()
get dragStartThreshold(): number {
return this._dragRef.dragStartThreshold;
}
set dragStartThreshold(val: number) {
this._dragRef.dragStartThreshold = coerceNumberProperty(val);
}

/** Number of CSS pixels that would be scrolled on each 'tick' when auto scroll is performed. */
@Input()
get scrollSpeed(): number { return this._dragRef.scrollSpeed; }
set scrollSpeed(value: number) {
this._dragRef.scrollSpeed = coerceNumberProperty(value, 2);
}

/**
* Parent element that contains the scroll. If an string is provided it would search that element by id on the dom.
* If no data provided or null autoscroll is not performed.
*/
@Input()
get scrollableParent(): HTMLElement | Document | string | null { return this._dragRef.scrollableParent; }
set scrollableParent(value: HTMLElement | Document | string | null) {
this._dragRef.scrollableParent = value;
}

/** Whether the item is draggable or not. Defaults to true. Does not affect manual dragging using the startDragManually method. */
@Input()
get draggable(): boolean {
return this._dragRef.draggable;
}
set draggable(val: boolean) {
this._dragRef.draggable = coerceBooleanProperty(val);
}

/**
* List of ids of grids or grid components that the item is connected to.
*/
@Input()
get connectedTo(): KtdGridComponent[] {
return this._connectedTo;
}
set connectedTo(val: (string|KtdGridComponent|any)[]) {
this._connectedTo = val.map((item: string|KtdGridComponent|any) => {
if (typeof item === 'string') {
const grid = this.registryService._ktgGrids.find(grid => grid.id === item);
if (grid === undefined) {
throw new Error(`KtdDrag connectedTo: could not find grid with id ${item}`);
}
return grid;
}
if (item instanceof KtdGridComponent) {
return item;
}
throw new Error(`KtdDrag connectedTo: connectedTo must be an array of KtdGridComponent or string`);
});
this.registryService.updateConnectedTo(this._dragRef, this._connectedTo);
}
private _connectedTo: KtdGridComponent[] = [];

@Input()
get id(): string {
return this._dragRef.id;
}
set id(val: string) {
this._dragRef.id = val;
}

/**
* Width of the draggable item, in cols. Minimum value is 1. Maximum value is how many cols the grid has.
*/
@Input()
get width(): number {
return this._dragRef.width;
}
set width(val: number) {
const width = coerceNumberProperty(val);
this._dragRef.width = width <= 0 ? 1 : width;
}

/**
* Height of the draggable item, in cols. Minimum value is 1. Maximum value is how many rows the grid has.
*/
@Input()
get height(): number {
return this._dragRef.height;
}
set height(val: number) {
const height = coerceNumberProperty(val);
this._dragRef.height = height <= 0 ? 1 : height;
}

@Input('ktdDragData')
set data(val: T) {
this._dragRef.data = val;
}

@Output('dragStart')
readonly dragStart: Observable<{source: DragRef<T>, event: PointingDeviceEvent}> = new Observable(
(observer: Observer<{source: DragRef<T>, event: PointingDeviceEvent}>) => {
const subscription = this._dragRef.dragStart$
.subscribe(observer);

return () => {
subscription.unsubscribe();
};
},
);

@Output('dragMove')
readonly dragMove: Observable<{source: DragRef<T>, event: PointingDeviceEvent}> = new Observable(
(observer: Observer<{source: DragRef<T>, event: PointingDeviceEvent}>) => {
const subscription = this._dragRef.dragMove$
.subscribe(observer);

return () => {
subscription.unsubscribe();
};
},
);

@Output('dragEnd')
readonly dragEnd: Observable<{source: DragRef<T>, event: PointingDeviceEvent}> = new Observable(
(observer: Observer<{source: DragRef<T>, event: PointingDeviceEvent}>) => {
const subscription = this._dragRef.dragEnd$
.subscribe(observer);

return () => {
subscription.unsubscribe();
};
},
);

public _dragRef: DragRef<T>;

private subscriptions: Subscription[] = [];

constructor(
/** Element that the draggable is attached to. */
public elementRef: ElementRef,
private gridService: KtdGridService,
private registryService: KtdRegistryService,
) {
this._dragRef = this.registryService.createKtgDrag(this.elementRef, this.gridService, this);
}

ngAfterContentInit(): void {
this.registryService.registerKtgDragItem(this);
this.subscriptions.push(
this._dragHandles.changes.subscribe(() => {
this._dragRef.dragHandles = this._dragHandles.toArray();
}),
this.dragStart.subscribe(({event}) => {
this.gridService.startDrag(event, this._dragRef, 'drag');
}),
);
}

ngOnDestroy(): void {
this.registryService.unregisterKtgDragItem(this);
this.registryService.destroyKtgDrag(this._dragRef);
this.subscriptions.forEach(subscription => subscription.unsubscribe());
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<ng-content></ng-content>
<div #resizeElem class="grid-item-resize-icon"></div>
<div #resizeElem class="grid-item-resize-icon"></div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
position: absolute;
z-index: 1;
overflow: hidden;
touch-action: none;

div {
position: absolute;
Expand Down
Loading