Skip to content

Commit

Permalink
feat(custom-drag-placeholders): added custom drag placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
Devin-Harris authored and llorenspujol committed Sep 21, 2022
1 parent a8b129d commit ce78265
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 49 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Use it in your template:
(layoutUpdated)="onLayoutUpdated($event)">
<ktd-grid-item *ngFor="let item of layout; trackBy:trackById" [id]="item.id">
<!-- Your grid item content goes here -->

<!-- Optional Custom placeholder template -->
<ng-template ktdGridItemPlaceholder>
<!-- Custom placeholder content goes here -->
</ng-template>
</ktd-grid-item>
</ktd-grid>
```
Expand Down Expand Up @@ -127,10 +132,14 @@ Here is listed the basic API of both KtdGridComponent and KtdGridItemComponent.
/** 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>();

```

#### KtdGridItem
```ts

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

Expand Down Expand Up @@ -163,9 +172,9 @@ Here is listed the basic API of both KtdGridComponent and KtdGridItemComponent.
- [x] Auto Scroll vertical/horizontal if container is scrollable when dragging a grid item. ([commit](https://github.com/katoid/angular-grid-layout/commit/d137d0e3f40cafdb5fdfd7b2bce4286670200c5d)).
- [x] Grid support for minWidth/maxWidth and minHeight/maxHeight on grid items.
- [x] Add grid gap feature.
- [x] Customizable drag placeholder.
- [ ] rowHeight to support also 'fit' as value instead of only CSS pixels ([issue](https://github.com/katoid/angular-grid-layout/issues/1)).
- [ ] Grid support for static grid items.
- [ ] Customizable drag placeholder.
- [ ] Check grid compact horizontal algorithm, estrange behaviour when overflowing, also in react-grid-layout.
- [ ] Add all other resize options (now is only available 'se-resize').
- [ ] Documentation.
Expand Down
24 changes: 24 additions & 0 deletions projects/angular-grid-layout/src/lib/directives/placeholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Directive, InjectionToken, Input, TemplateRef } from '@angular/core';

/**
* Injection token that can be used to reference instances of `KtdGridItemPlaceholder`. It serves as
* alternative token to the actual `KtdGridItemPlaceholder` class which could cause unnecessary
* retention of the class and its directive metadata.
*/
export const KTD_GRID_ITEM_PLACEHOLDER = new InjectionToken<KtdGridItemPlaceholder>('KtdGridItemPlaceholder');

/** Directive that can be used to create a custom placeholder for a KtdGridItem instance. */
@Directive({
selector: 'ng-template[ktdGridItemPlaceholder]',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
class: 'ktd-grid-item-placeholder-content'
},
providers: [{provide: KTD_GRID_ITEM_PLACEHOLDER, useExisting: KtdGridItemPlaceholder}],
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class KtdGridItemPlaceholder<T = any> {
/** Context data to be added to the placeholder template instance. */
@Input() data: T;
constructor(public templateRef: TemplateRef<T>) {}
}
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
@@ -1,6 +1,6 @@
import {
AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, QueryList, Renderer2,
ViewChild
AfterContentInit, ChangeDetectionStrategy, Component, ContentChild, ContentChildren, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit,
QueryList, Renderer2, ViewChild
} from '@angular/core';
import { BehaviorSubject, iif, merge, NEVER, Observable, Subject, Subscription } from 'rxjs';
import { exhaustMap, filter, map, startWith, switchMap, take, takeUntil } from 'rxjs/operators';
Expand All @@ -12,6 +12,7 @@ import { KtdGridService } from '../grid.service';
import { ktdOutsideZone } from '../utils/operators';
import { BooleanInput, coerceBooleanProperty } from '../coercion/boolean-property';
import { coerceNumberProperty, NumberInput } from '../coercion/number-property';
import { KTD_GRID_ITEM_PLACEHOLDER, KtdGridItemPlaceholder } from '../directives/placeholder';

@Component({
selector: 'ktd-grid-item',
Expand All @@ -25,6 +26,9 @@ export class KtdGridItemComponent implements OnInit, OnDestroy, AfterContentInit
@ContentChildren(KTD_GRID_RESIZE_HANDLE, {descendants: true}) _resizeHandles: QueryList<KtdGridResizeHandle>;
@ViewChild('resizeElem', {static: true, read: ElementRef}) resizeElem: ElementRef;

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

/** Min and max size input properties. Any of these would 'override' the min/max values specified in the layout. */
@Input() minW?: number;
@Input() minH?: number;
Expand Down
2 changes: 1 addition & 1 deletion projects/angular-grid-layout/src/lib/grid.component.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<ng-content></ng-content>
<ng-content></ng-content>
7 changes: 5 additions & 2 deletions projects/angular-grid-layout/src/lib/grid.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ ktd-grid {

.ktd-grid-item-placeholder {
position: absolute;
background-color: darkred;
opacity: 0.6;
z-index: 0;
transition-property: transform;
transition: all 150ms ease;

&-default {
background-color: darkred;
opacity: 0.6;
}
}
}
119 changes: 88 additions & 31 deletions projects/angular-grid-layout/src/lib/grid.component.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import {
AfterContentChecked, AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, Input, NgZone, OnChanges,
OnDestroy, Output, QueryList, Renderer2, SimpleChanges, ViewEncapsulation
AfterContentChecked, AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EmbeddedViewRef, EventEmitter, Input,
NgZone, OnChanges, OnDestroy, Output, QueryList, Renderer2, SimpleChanges, ViewContainerRef, ViewEncapsulation
} from '@angular/core';
import { coerceNumberProperty, NumberInput } from './coercion/number-property';
import { KtdGridItemComponent } from './grid-item/grid-item.component';
import { combineLatest, merge, NEVER, Observable, Observer, of, Subscription } from 'rxjs';
import { exhaustMap, map, startWith, switchMap, takeUntil } from 'rxjs/operators';
import { ktdGridItemDragging, ktdGridItemResizing } from './utils/grid.utils';
import { compact, CompactType } from './utils/react-grid-layout.utils';
import { ktdGridItemDragging, ktdGridItemLayoutItemAreEqual, ktdGridItemResizing } from './utils/grid.utils';
import { compact } from './utils/react-grid-layout.utils';
import {
GRID_ITEM_GET_RENDER_DATA_TOKEN, KtdDraggingData, KtdGridCfg, KtdGridCompactType, KtdGridItemRect, KtdGridItemRenderData, KtdGridLayout,
KtdGridLayoutItem
GRID_ITEM_GET_RENDER_DATA_TOKEN, KtdGridCfg, KtdGridCompactType, KtdGridItemRenderData, KtdGridLayout, KtdGridLayoutItem
} from './grid.definitions';
import { ktdMouseOrTouchEnd, ktdPointerClientX, ktdPointerClientY } from './utils/pointer.utils';
import { KtdDictionary } from '../types';
import { KtdGridService } from './grid.service';
import { getMutableClientRect, KtdClientRect } from './utils/client-rect';
import { ktdGetScrollTotalRelativeDifference$, ktdScrollIfNearElementClientRect$ } from './utils/scroll';
import { BooleanInput, coerceBooleanProperty } from './coercion/boolean-property';
import { KtdGridItemPlaceholder } from './directives/placeholder';

interface KtdDragResizeEvent {
layout: KtdGridLayout;
Expand All @@ -30,6 +30,14 @@ export type KtdResizeStart = KtdDragResizeEvent;
export type KtdDragEnd = KtdDragResizeEvent;
export type KtdResizeEnd = KtdDragResizeEvent;

export interface KtdGridItemResizeEvent {
width: number;
height: number;
gridItemRef: KtdGridItemComponent;
}

type DragActionType = 'drag' | 'resize';

function getDragResizeEventData(gridItem: KtdGridItemComponent, layout: KtdGridLayout): KtdDragResizeEvent {
return {
layout,
Expand Down Expand Up @@ -118,6 +126,9 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
/** 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>();

/**
* 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.
Expand Down Expand Up @@ -227,13 +238,20 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
};
}

/** Reference to the view of the placeholder element. */
private placeholderRef: EmbeddedViewRef<any> | null;

/** Element that is rendered as placeholder when a grid item is being dragged */
private placeholder: HTMLElement | null;

/** Total height of the grid */
private _height: number;
private _gridItemsRenderData: KtdDictionary<KtdGridItemRenderData<number>>;
private subscriptions: Subscription[];

constructor(private gridService: KtdGridService,
private elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
private renderer: Renderer2,
private ngZone: NgZone) {

Expand Down Expand Up @@ -323,18 +341,15 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
startWith(this._gridItems),
switchMap((gridItems: QueryList<KtdGridItemComponent>) => {
return merge(
...gridItems.map((gridItem) => gridItem.dragStart$.pipe(map((event) => ({event, gridItem, type: 'drag'})))),
...gridItems.map((gridItem) => gridItem.resizeStart$.pipe(map((event) => ({event, gridItem, type: 'resize'})))),
...gridItems.map((gridItem) => gridItem.dragStart$.pipe(map((event) => ({event, gridItem, type: 'drag' as DragActionType})))),
...gridItems.map((gridItem) => gridItem.resizeStart$.pipe(map((event) => ({event, gridItem, type: 'resize' as DragActionType})))),
).pipe(exhaustMap(({event, gridItem, type}) => {
// Emit drag or resize start events. Ensure that is start event is inside the zone.
this.ngZone.run(() => (type === 'drag' ? this.dragStarted : this.resizeStarted).emit(getDragResizeEventData(gridItem, this.layout)));
// Get the correct newStateFunc depending on if we are dragging or resizing
const calcNewStateFunc = type === 'drag' ? ktdGridItemDragging : ktdGridItemResizing;

// Perform drag sequence
return this.performDragSequence$(gridItem, event, (gridItemId, config, compactionType, draggingData) =>
calcNewStateFunc(gridItem, config, compactionType, draggingData)
).pipe(map((layout) => ({layout, gridItem, type})));
return this.performDragSequence$(gridItem, event, type).pipe(
map((layout) => ({layout, gridItem, type})));

}));
})
Expand All @@ -358,8 +373,7 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
* @param pointerDownEvent event (mousedown or touchdown) where the user initiated the drag
* @param calcNewStateFunc function that return the new layout state and the drag element position
*/
private performDragSequence$(gridItem: KtdGridItemComponent, pointerDownEvent: MouseEvent | TouchEvent,
calcNewStateFunc: (gridItem: KtdGridItemComponent, config: KtdGridCfg, compactionType: CompactType, draggingData: KtdDraggingData) => { layout: KtdGridLayoutItem[]; draggedItemPos: KtdGridItemRect }): Observable<KtdGridLayout> {
private performDragSequence$(gridItem: KtdGridItemComponent, pointerDownEvent: MouseEvent | TouchEvent, type: DragActionType): Observable<KtdGridLayout> {

return new Observable<KtdGridLayout>((observer: Observer<KtdGridLayout>) => {
// Retrieve grid (parent) and gridItem (draggedElem) client rects.
Expand All @@ -371,14 +385,12 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
this.renderer.addClass(gridItem.elementRef.nativeElement, 'no-transitions');
this.renderer.addClass(gridItem.elementRef.nativeElement, 'ktd-grid-item-dragging');

// Create placeholder element. This element would represent the position where the dragged/resized element would be if the action ends
const placeholderElement: HTMLDivElement = this.renderer.createElement('div');
placeholderElement.style.width = `${dragElemClientRect.width}px`;
placeholderElement.style.height = `${dragElemClientRect.height}px`;
placeholderElement.style.transform = `translateX(${dragElemClientRect.left - gridElemClientRect.left}px) translateY(${dragElemClientRect.top - gridElemClientRect.top}px)`;

this.renderer.addClass(placeholderElement, 'ktd-grid-item-placeholder');
this.renderer.appendChild(this.elementRef.nativeElement, placeholderElement);
const placeholderClientRect: KtdClientRect = {
...dragElemClientRect,
left: dragElemClientRect.left - gridElemClientRect.left,
top: dragElemClientRect.top - gridElemClientRect.top
}
this.createPlaceholderElement(placeholderClientRect, gridItem.placeholder);

let newLayout: KtdGridLayoutItem[];

Expand Down Expand Up @@ -421,6 +433,9 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
*/
const currentLayout: KtdGridLayout = newLayout || this.layout;

// Get the correct newStateFunc depending on if we are dragging or resizing
const calcNewStateFunc = type === 'drag' ? ktdGridItemDragging : ktdGridItemResizing;

const {layout, draggedItemPos} = calcNewStateFunc(gridItem, {
layout: currentLayout,
rowHeight: this.rowHeight,
Expand All @@ -446,12 +461,13 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
gap: this.gap,
}, gridElemClientRect.width, gridElemClientRect.height);

const placeholderStyles = parseRenderItemToPixels(this._gridItemsRenderData[gridItem.id]);
const newGridItemRenderData = {...this._gridItemsRenderData[gridItem.id]}
const placeholderStyles = parseRenderItemToPixels(newGridItemRenderData);

// Put the real final position to the placeholder element
placeholderElement.style.width = placeholderStyles.width;
placeholderElement.style.height = placeholderStyles.height;
placeholderElement.style.transform = `translateX(${placeholderStyles.left}) translateY(${placeholderStyles.top})`;
this.placeholder!.style.width = placeholderStyles.width;
this.placeholder!.style.height = placeholderStyles.height;
this.placeholder!.style.transform = `translateX(${placeholderStyles.left}) translateY(${placeholderStyles.top})`;

// modify the position of the dragged item to be the once we want (for example the mouse position or whatever)
this._gridItemsRenderData[gridItem.id] = {
Expand All @@ -460,6 +476,21 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
};

this.render();

// If we are performing a resize, and bounds have changed, emit event.
// NOTE: Only emit on resize for now. Use case for normal drag is not justified for now. Emitting on resize is, since we may want to re-render the grid item or the placeholder in order to fit the new bounds.
if (type === 'resize') {
const prevGridItem = currentLayout.find(item => item.id === gridItem.id)!;
const newGridItem = newLayout.find(item => item.id === gridItem.id)!;
// Check if item resized has changed, if so, emit resize change event
if (!ktdGridItemLayoutItemAreEqual(prevGridItem, newGridItem)) {
this.gridItemResize.emit({
width: newGridItemRenderData.width,
height: newGridItemRenderData.height,
gridItemRef: getDragResizeEventData(gridItem, newLayout).gridItemRef
});
}
}
},
(error) => observer.error(error),
() => {
Expand All @@ -468,10 +499,7 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
this.renderer.removeClass(gridItem.elementRef.nativeElement, 'no-transitions');
this.renderer.removeClass(gridItem.elementRef.nativeElement, 'ktd-grid-item-dragging');

// Remove placeholder element from the dom
// NOTE: If we don't put the removeChild inside the zone it would not work... This may be a bug from angular or maybe is the intended behaviour, although strange.
// It should work since AFAIK this action should not be done in a CD cycle.
this.renderer.removeChild(this.elementRef.nativeElement, placeholderElement);
this.destroyPlaceholder();

if (newLayout) {
// TODO: newLayout should already be pruned. If not, it should have type Layout, not KtdGridLayout as it is now.
Expand Down Expand Up @@ -505,6 +533,35 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
});
}

/** Creates placeholder element */
private createPlaceholderElement(clientRect: KtdClientRect, gridItemPlaceholder?: KtdGridItemPlaceholder) {
this.placeholder = this.renderer.createElement('div');
this.placeholder!.style.width = `${clientRect.width}px`;
this.placeholder!.style.height = `${clientRect.height}px`;
this.placeholder!.style.transform = `translateX(${clientRect.left}px) translateY(${clientRect.top}px)`;
this.placeholder!.classList.add('ktd-grid-item-placeholder');
this.renderer.appendChild(this.elementRef.nativeElement, this.placeholder);

// Create and append custom placeholder if provided.
// Important: Append it after creating & appending the container placeholder. This way we ensure parent bounds are set when creating the embeddedView.
if (gridItemPlaceholder) {
this.placeholderRef = this.viewContainerRef.createEmbeddedView(
gridItemPlaceholder.templateRef,
gridItemPlaceholder.data
);
this.placeholderRef.rootNodes.forEach(node => this.placeholder!.appendChild(node));
this.placeholderRef.detectChanges();
} else {
this.placeholder!.classList.add('ktd-grid-item-placeholder-default');
}
}

/** Destroys the placeholder element and its ViewRef. */
private destroyPlaceholder() {
this.placeholder?.remove();
this.placeholderRef?.destroy();
this.placeholder = this.placeholderRef = null!;
}

static ngAcceptInputType_cols: NumberInput;
static ngAcceptInputType_rowHeight: NumberInput;
Expand Down
Loading

0 comments on commit ce78265

Please sign in to comment.