Skip to content

Commit ce78265

Browse files
Devin-Harrisllorenspujol
authored andcommitted
feat(custom-drag-placeholders): added custom drag placeholders
1 parent a8b129d commit ce78265

16 files changed

+289
-49
lines changed

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ Use it in your template:
5757
(layoutUpdated)="onLayoutUpdated($event)">
5858
<ktd-grid-item *ngFor="let item of layout; trackBy:trackById" [id]="item.id">
5959
<!-- Your grid item content goes here -->
60+
61+
<!-- Optional Custom placeholder template -->
62+
<ng-template ktdGridItemPlaceholder>
63+
<!-- Custom placeholder content goes here -->
64+
</ng-template>
6065
</ktd-grid-item>
6166
</ktd-grid>
6267
```
@@ -127,10 +132,14 @@ Here is listed the basic API of both KtdGridComponent and KtdGridItemComponent.
127132
/** Emits when resize ends */
128133
@Output() resizeEnded: EventEmitter<KtdResizeEnd> = new EventEmitter<KtdResizeEnd>();
129134

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

132140
#### KtdGridItem
133141
```ts
142+
134143
/** Id of the grid item. This property is strictly compulsory. */
135144
@Input() id: string;
136145

@@ -163,9 +172,9 @@ Here is listed the basic API of both KtdGridComponent and KtdGridItemComponent.
163172
- [x] Auto Scroll vertical/horizontal if container is scrollable when dragging a grid item. ([commit](https://github.com/katoid/angular-grid-layout/commit/d137d0e3f40cafdb5fdfd7b2bce4286670200c5d)).
164173
- [x] Grid support for minWidth/maxWidth and minHeight/maxHeight on grid items.
165174
- [x] Add grid gap feature.
175+
- [x] Customizable drag placeholder.
166176
- [ ] rowHeight to support also 'fit' as value instead of only CSS pixels ([issue](https://github.com/katoid/angular-grid-layout/issues/1)).
167177
- [ ] Grid support for static grid items.
168-
- [ ] Customizable drag placeholder.
169178
- [ ] Check grid compact horizontal algorithm, estrange behaviour when overflowing, also in react-grid-layout.
170179
- [ ] Add all other resize options (now is only available 'se-resize').
171180
- [ ] Documentation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Directive, InjectionToken, Input, TemplateRef } from '@angular/core';
2+
3+
/**
4+
* Injection token that can be used to reference instances of `KtdGridItemPlaceholder`. It serves as
5+
* alternative token to the actual `KtdGridItemPlaceholder` class which could cause unnecessary
6+
* retention of the class and its directive metadata.
7+
*/
8+
export const KTD_GRID_ITEM_PLACEHOLDER = new InjectionToken<KtdGridItemPlaceholder>('KtdGridItemPlaceholder');
9+
10+
/** Directive that can be used to create a custom placeholder for a KtdGridItem instance. */
11+
@Directive({
12+
selector: 'ng-template[ktdGridItemPlaceholder]',
13+
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
14+
host: {
15+
class: 'ktd-grid-item-placeholder-content'
16+
},
17+
providers: [{provide: KTD_GRID_ITEM_PLACEHOLDER, useExisting: KtdGridItemPlaceholder}],
18+
})
19+
// eslint-disable-next-line @angular-eslint/directive-class-suffix
20+
export class KtdGridItemPlaceholder<T = any> {
21+
/** Context data to be added to the placeholder template instance. */
22+
@Input() data: T;
23+
constructor(public templateRef: TemplateRef<T>) {}
24+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
<ng-content></ng-content>
2-
<div #resizeElem class="grid-item-resize-icon"></div>
2+
<div #resizeElem class="grid-item-resize-icon"></div>

projects/angular-grid-layout/src/lib/grid-item/grid-item.component.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
2-
AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, QueryList, Renderer2,
3-
ViewChild
2+
AfterContentInit, ChangeDetectionStrategy, Component, ContentChild, ContentChildren, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit,
3+
QueryList, Renderer2, ViewChild
44
} from '@angular/core';
55
import { BehaviorSubject, iif, merge, NEVER, Observable, Subject, Subscription } from 'rxjs';
66
import { exhaustMap, filter, map, startWith, switchMap, take, takeUntil } from 'rxjs/operators';
@@ -12,6 +12,7 @@ import { KtdGridService } from '../grid.service';
1212
import { ktdOutsideZone } from '../utils/operators';
1313
import { BooleanInput, coerceBooleanProperty } from '../coercion/boolean-property';
1414
import { coerceNumberProperty, NumberInput } from '../coercion/number-property';
15+
import { KTD_GRID_ITEM_PLACEHOLDER, KtdGridItemPlaceholder } from '../directives/placeholder';
1516

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

29+
/** Template ref for placeholder */
30+
@ContentChild(KTD_GRID_ITEM_PLACEHOLDER) placeholder: KtdGridItemPlaceholder;
31+
2832
/** Min and max size input properties. Any of these would 'override' the min/max values specified in the layout. */
2933
@Input() minW?: number;
3034
@Input() minH?: number;
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<ng-content></ng-content>
1+
<ng-content></ng-content>

projects/angular-grid-layout/src/lib/grid.component.scss

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ ktd-grid {
1515

1616
.ktd-grid-item-placeholder {
1717
position: absolute;
18-
background-color: darkred;
19-
opacity: 0.6;
2018
z-index: 0;
2119
transition-property: transform;
2220
transition: all 150ms ease;
21+
22+
&-default {
23+
background-color: darkred;
24+
opacity: 0.6;
25+
}
2326
}
2427
}

projects/angular-grid-layout/src/lib/grid.component.ts

+88-31
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import {
2-
AfterContentChecked, AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, Input, NgZone, OnChanges,
3-
OnDestroy, Output, QueryList, Renderer2, SimpleChanges, ViewEncapsulation
2+
AfterContentChecked, AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EmbeddedViewRef, EventEmitter, Input,
3+
NgZone, OnChanges, OnDestroy, Output, QueryList, Renderer2, SimpleChanges, ViewContainerRef, ViewEncapsulation
44
} from '@angular/core';
55
import { coerceNumberProperty, NumberInput } from './coercion/number-property';
66
import { KtdGridItemComponent } from './grid-item/grid-item.component';
77
import { combineLatest, merge, NEVER, Observable, Observer, of, Subscription } from 'rxjs';
88
import { exhaustMap, map, startWith, switchMap, takeUntil } from 'rxjs/operators';
9-
import { ktdGridItemDragging, ktdGridItemResizing } from './utils/grid.utils';
10-
import { compact, CompactType } from './utils/react-grid-layout.utils';
9+
import { ktdGridItemDragging, ktdGridItemLayoutItemAreEqual, ktdGridItemResizing } from './utils/grid.utils';
10+
import { compact } from './utils/react-grid-layout.utils';
1111
import {
12-
GRID_ITEM_GET_RENDER_DATA_TOKEN, KtdDraggingData, KtdGridCfg, KtdGridCompactType, KtdGridItemRect, KtdGridItemRenderData, KtdGridLayout,
13-
KtdGridLayoutItem
12+
GRID_ITEM_GET_RENDER_DATA_TOKEN, KtdGridCfg, KtdGridCompactType, KtdGridItemRenderData, KtdGridLayout, KtdGridLayoutItem
1413
} from './grid.definitions';
1514
import { ktdMouseOrTouchEnd, ktdPointerClientX, ktdPointerClientY } from './utils/pointer.utils';
1615
import { KtdDictionary } from '../types';
1716
import { KtdGridService } from './grid.service';
1817
import { getMutableClientRect, KtdClientRect } from './utils/client-rect';
1918
import { ktdGetScrollTotalRelativeDifference$, ktdScrollIfNearElementClientRect$ } from './utils/scroll';
2019
import { BooleanInput, coerceBooleanProperty } from './coercion/boolean-property';
20+
import { KtdGridItemPlaceholder } from './directives/placeholder';
2121

2222
interface KtdDragResizeEvent {
2323
layout: KtdGridLayout;
@@ -30,6 +30,14 @@ export type KtdResizeStart = KtdDragResizeEvent;
3030
export type KtdDragEnd = KtdDragResizeEvent;
3131
export type KtdResizeEnd = KtdDragResizeEvent;
3232

33+
export interface KtdGridItemResizeEvent {
34+
width: number;
35+
height: number;
36+
gridItemRef: KtdGridItemComponent;
37+
}
38+
39+
type DragActionType = 'drag' | 'resize';
40+
3341
function getDragResizeEventData(gridItem: KtdGridItemComponent, layout: KtdGridLayout): KtdDragResizeEvent {
3442
return {
3543
layout,
@@ -118,6 +126,9 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
118126
/** Emits when resize ends */
119127
@Output() resizeEnded: EventEmitter<KtdResizeEnd> = new EventEmitter<KtdResizeEnd>();
120128

129+
/** Emits when a grid item is being resized and its bounds have changed */
130+
@Output() gridItemResize: EventEmitter<KtdGridItemResizeEvent> = new EventEmitter<KtdGridItemResizeEvent>();
131+
121132
/**
122133
* Parent element that contains the scroll. If an string is provided it would search that element by id on the dom.
123134
* If no data provided or null autoscroll is not performed.
@@ -227,13 +238,20 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
227238
};
228239
}
229240

241+
/** Reference to the view of the placeholder element. */
242+
private placeholderRef: EmbeddedViewRef<any> | null;
243+
244+
/** Element that is rendered as placeholder when a grid item is being dragged */
245+
private placeholder: HTMLElement | null;
246+
230247
/** Total height of the grid */
231248
private _height: number;
232249
private _gridItemsRenderData: KtdDictionary<KtdGridItemRenderData<number>>;
233250
private subscriptions: Subscription[];
234251

235252
constructor(private gridService: KtdGridService,
236253
private elementRef: ElementRef,
254+
private viewContainerRef: ViewContainerRef,
237255
private renderer: Renderer2,
238256
private ngZone: NgZone) {
239257

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

334350
// Perform drag sequence
335-
return this.performDragSequence$(gridItem, event, (gridItemId, config, compactionType, draggingData) =>
336-
calcNewStateFunc(gridItem, config, compactionType, draggingData)
337-
).pipe(map((layout) => ({layout, gridItem, type})));
351+
return this.performDragSequence$(gridItem, event, type).pipe(
352+
map((layout) => ({layout, gridItem, type})));
338353

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

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

374-
// Create placeholder element. This element would represent the position where the dragged/resized element would be if the action ends
375-
const placeholderElement: HTMLDivElement = this.renderer.createElement('div');
376-
placeholderElement.style.width = `${dragElemClientRect.width}px`;
377-
placeholderElement.style.height = `${dragElemClientRect.height}px`;
378-
placeholderElement.style.transform = `translateX(${dragElemClientRect.left - gridElemClientRect.left}px) translateY(${dragElemClientRect.top - gridElemClientRect.top}px)`;
379-
380-
this.renderer.addClass(placeholderElement, 'ktd-grid-item-placeholder');
381-
this.renderer.appendChild(this.elementRef.nativeElement, placeholderElement);
388+
const placeholderClientRect: KtdClientRect = {
389+
...dragElemClientRect,
390+
left: dragElemClientRect.left - gridElemClientRect.left,
391+
top: dragElemClientRect.top - gridElemClientRect.top
392+
}
393+
this.createPlaceholderElement(placeholderClientRect, gridItem.placeholder);
382394

383395
let newLayout: KtdGridLayoutItem[];
384396

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

436+
// Get the correct newStateFunc depending on if we are dragging or resizing
437+
const calcNewStateFunc = type === 'drag' ? ktdGridItemDragging : ktdGridItemResizing;
438+
424439
const {layout, draggedItemPos} = calcNewStateFunc(gridItem, {
425440
layout: currentLayout,
426441
rowHeight: this.rowHeight,
@@ -446,12 +461,13 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
446461
gap: this.gap,
447462
}, gridElemClientRect.width, gridElemClientRect.height);
448463

449-
const placeholderStyles = parseRenderItemToPixels(this._gridItemsRenderData[gridItem.id]);
464+
const newGridItemRenderData = {...this._gridItemsRenderData[gridItem.id]}
465+
const placeholderStyles = parseRenderItemToPixels(newGridItemRenderData);
450466

451467
// Put the real final position to the placeholder element
452-
placeholderElement.style.width = placeholderStyles.width;
453-
placeholderElement.style.height = placeholderStyles.height;
454-
placeholderElement.style.transform = `translateX(${placeholderStyles.left}) translateY(${placeholderStyles.top})`;
468+
this.placeholder!.style.width = placeholderStyles.width;
469+
this.placeholder!.style.height = placeholderStyles.height;
470+
this.placeholder!.style.transform = `translateX(${placeholderStyles.left}) translateY(${placeholderStyles.top})`;
455471

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

462478
this.render();
479+
480+
// If we are performing a resize, and bounds have changed, emit event.
481+
// 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.
482+
if (type === 'resize') {
483+
const prevGridItem = currentLayout.find(item => item.id === gridItem.id)!;
484+
const newGridItem = newLayout.find(item => item.id === gridItem.id)!;
485+
// Check if item resized has changed, if so, emit resize change event
486+
if (!ktdGridItemLayoutItemAreEqual(prevGridItem, newGridItem)) {
487+
this.gridItemResize.emit({
488+
width: newGridItemRenderData.width,
489+
height: newGridItemRenderData.height,
490+
gridItemRef: getDragResizeEventData(gridItem, newLayout).gridItemRef
491+
});
492+
}
493+
}
463494
},
464495
(error) => observer.error(error),
465496
() => {
@@ -468,10 +499,7 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
468499
this.renderer.removeClass(gridItem.elementRef.nativeElement, 'no-transitions');
469500
this.renderer.removeClass(gridItem.elementRef.nativeElement, 'ktd-grid-item-dragging');
470501

471-
// Remove placeholder element from the dom
472-
// 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.
473-
// It should work since AFAIK this action should not be done in a CD cycle.
474-
this.renderer.removeChild(this.elementRef.nativeElement, placeholderElement);
502+
this.destroyPlaceholder();
475503

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

536+
/** Creates placeholder element */
537+
private createPlaceholderElement(clientRect: KtdClientRect, gridItemPlaceholder?: KtdGridItemPlaceholder) {
538+
this.placeholder = this.renderer.createElement('div');
539+
this.placeholder!.style.width = `${clientRect.width}px`;
540+
this.placeholder!.style.height = `${clientRect.height}px`;
541+
this.placeholder!.style.transform = `translateX(${clientRect.left}px) translateY(${clientRect.top}px)`;
542+
this.placeholder!.classList.add('ktd-grid-item-placeholder');
543+
this.renderer.appendChild(this.elementRef.nativeElement, this.placeholder);
544+
545+
// Create and append custom placeholder if provided.
546+
// Important: Append it after creating & appending the container placeholder. This way we ensure parent bounds are set when creating the embeddedView.
547+
if (gridItemPlaceholder) {
548+
this.placeholderRef = this.viewContainerRef.createEmbeddedView(
549+
gridItemPlaceholder.templateRef,
550+
gridItemPlaceholder.data
551+
);
552+
this.placeholderRef.rootNodes.forEach(node => this.placeholder!.appendChild(node));
553+
this.placeholderRef.detectChanges();
554+
} else {
555+
this.placeholder!.classList.add('ktd-grid-item-placeholder-default');
556+
}
557+
}
558+
559+
/** Destroys the placeholder element and its ViewRef. */
560+
private destroyPlaceholder() {
561+
this.placeholder?.remove();
562+
this.placeholderRef?.destroy();
563+
this.placeholder = this.placeholderRef = null!;
564+
}
508565

509566
static ngAcceptInputType_cols: NumberInput;
510567
static ngAcceptInputType_rowHeight: NumberInput;

0 commit comments

Comments
 (0)