Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
212 changes: 212 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,212 @@
import {
AfterContentInit, ContentChild, ContentChildren,
Directive, ElementRef,
InjectionToken, Input, OnDestroy, Output, QueryList
} from '@angular/core';
import {coerceBooleanProperty} from "../coercion/boolean-property";
import {BehaviorSubject, 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 {ktdPointerClientX, ktdPointerClientY} from "../utils/pointer.utils";
import {takeUntil} from "rxjs/operators";


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._dragStartThreshold;
}
set dragStartThreshold(val: number) {
this._dragStartThreshold = coerceNumberProperty(val);
}
private _dragStartThreshold: number = 0;

/** 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._draggable;
}
set draggable(val: boolean) {
this._draggable = coerceBooleanProperty(val);
this._dragRef.draggable = this._draggable;
this._draggable$.next(this._draggable);
}
private _draggable: boolean = true;
private _draggable$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(this._draggable);

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

/**
* Width of the draggable item, in cols. When set to 0 we will use the width of grid.
*/
@Input()
get width(): number {
return this._dragRef.width;
}
set width(val: number) {
this._dragRef.width = coerceNumberProperty(val);
}

/**
* Height of the draggable item, in cols. When set to 0 we will use the height of grid.
*/
@Input()
get height(): number {
return this._dragRef.height;
}
set height(val: number) {
this._dragRef.height = coerceNumberProperty(val);
}

/**
* TODO: Add support for custom drag data.
*/
@Input('ktdDragData') data: T;

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

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

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

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

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

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

public _dragRef: DragRef<T>;
private subscriptions: Subscription[] = [];
private element: HTMLElement;

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

ngAfterContentInit(): void {
this.element = this.elementRef.nativeElement as HTMLElement;
this.registryService.registerKtgDragItem(this);
this.initDrag();
}

ngOnDestroy(): void {
this.registryService.unregisterKtgDragItem(this);
this.registryService.destroyKtgDrag(this._dragRef);
this.subscriptions.forEach(subscription => subscription.unsubscribe());
}

/**
* Initialize the drag of ktd-drag element, placeholder dragging is handled by ktd-grid.
* The element will be freely draggable, when drag ends it will snap back to its initial place.
*/
private initDrag() {
this._dragRef.dragStartThreshold = this.dragStartThreshold;
this._dragRef.placeholder = this.placeholder;
this._dragRef.draggable = this.draggable;
this._dragRef.dragHandles = this._dragHandles.toArray();

const handlesSub$ = this._dragHandles.changes.subscribe(() => {
console.log(this._dragHandles.toArray());
this._dragRef.dragHandles = this._dragHandles.toArray();
});

const dragStart$ = this.dragStart.subscribe(({event}) => {
let currentX = 0,
currentY = 0;

const initialX = ktdPointerClientX(event) - currentX;
const initialY = ktdPointerClientY(event) - currentY;

this.dragMove.pipe(
takeUntil(this.dragEnd),
).subscribe(({event}) => {
event.preventDefault();

// Calculate the new cursor position:
currentX = ktdPointerClientX(event) - initialX;
currentY = ktdPointerClientY(event) - initialY;

/**
* TODO: Add support handling scroll offset
*
* Possible solution would be to add for each scrollParent element observable that emits scroll offset,
* and use the offset when we are on top of the scrollParent.
* Still dont know how we would handle nested scrollParents, generated by nested grids.
*/

this.element.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, ktdDrag manages its drag movement, whereas the drag movement of the ktd-grid-item is controlled by the ktd-grid. This creates a bit of inconsistency. I believe that the approach you're taking here is correct, so we should enable the ktd-grid-item to handle its drag movement as well.

This will also open the door for drag-and-drop functionality between grids in the near future.

});
});

const dragEnd$ = this.dragEnd.subscribe(() => {
this.element.style.transform = 'none';
});

this.subscriptions.push(handlesSub$, dragStart$, dragEnd$);
}
}
Loading