diff --git a/projects/angular-grid-layout/src/lib/grid.component.ts b/projects/angular-grid-layout/src/lib/grid.component.ts index eaab3c3..fd32663 100644 --- a/projects/angular-grid-layout/src/lib/grid.component.ts +++ b/projects/angular-grid-layout/src/lib/grid.component.ts @@ -6,7 +6,7 @@ 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, ktdGridItemLayoutItemAreEqual, ktdGridItemResizing } from './utils/grid.utils'; +import { ktdGetGridItemRowHeight, ktdGridItemDragging, ktdGridItemLayoutItemAreEqual, ktdGridItemResizing } from './utils/grid.utils'; import { compact } from './utils/react-grid-layout.utils'; import { GRID_ITEM_GET_RENDER_DATA_TOKEN, KtdGridCfg, KtdGridCompactType, KtdGridItemRenderData, KtdGridLayout, KtdGridLayoutItem @@ -49,16 +49,17 @@ function getDragResizeEventData(gridItem: KtdGridItemComponent, layout: KtdGridL function layoutToRenderItems(config: KtdGridCfg, width: number, height: number): KtdDictionary> { const {cols, rowHeight, layout, gap} = config; - const widthExcludinggap = width - Math.max((gap * (cols - 1)), 0); - const itemWidthPerColumn = (widthExcludinggap / cols); + const rowHeightInPixels = rowHeight === 'fit' ? ktdGetGridItemRowHeight(layout, height, gap) : rowHeight; + const widthExcludingGap = width - Math.max((gap * (cols - 1)), 0); + const itemWidthPerColumn = (widthExcludingGap / cols); const renderItems: KtdDictionary> = {}; for (const item of layout) { renderItems[item.id] = { id: item.id, - top: item.y * rowHeight + gap * item.y, + top: item.y * rowHeightInPixels + gap * item.y, left: item.x * itemWidthPerColumn + gap * item.x, width: item.w * itemWidthPerColumn + gap * Math.max(item.w - 1, 0), - height: item.h * rowHeight + gap * Math.max(item.h - 1, 0), + height: item.h * rowHeightInPixels + gap * Math.max(item.h - 1, 0), }; } return renderItems; @@ -228,10 +229,24 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte private _gap: number = 0; + @Input() + get height(): number | null { + return this._height; + } + + set height(val: number | null) { + this._height = typeof val === 'number' ? Math.max(val, 0) : null; + } + + /** Total height of the grid */ + private _height: number | null = null; + private gridCurrentHeight: number; + get config(): KtdGridCfg { return { cols: this.cols, - rowHeight: this.rowHeight as any, + rowHeight: this.rowHeight, + height: this.height, layout: this.layout, preventCollision: this.preventCollision, gap: this.gap, @@ -244,8 +259,6 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte /** 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>; private subscriptions: Subscription[]; @@ -258,6 +271,11 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte } ngOnChanges(changes: SimpleChanges) { + + if (this.rowHeight === 'fit' && this.height == null) { + console.warn(`KtdGridComponent: The @Input() height should not be null when using rowHeight 'fit'`); + } + let needsCompactLayout = false; let needsRecalculateRenderData = false; @@ -268,7 +286,7 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte } // Check if wee need to recalculate rendering data. - if (needsCompactLayout || changes.rowHeight || changes.gap) { + if (needsCompactLayout || changes.rowHeight || changes.height || changes.gap) { needsRecalculateRenderData = true; } @@ -315,24 +333,12 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte calculateRenderData() { const clientRect = (this.elementRef.nativeElement as HTMLElement).getBoundingClientRect(); - if (this.rowHeight === 'fit') { - const totalRows = getGridHeight(this.layout, 1, this.gap); - this._height = clientRect.height; - this._gridItemsRenderData = layoutToRenderItems(this.config, clientRect.width, clientRect.height); - } else { - this._gridItemsRenderData = layoutToRenderItems(this.config, clientRect.width, clientRect.height); - this._height = getGridHeight(this.layout, this.rowHeight, this.gap); - } + this.gridCurrentHeight = this.height ?? (this.rowHeight === 'fit' ? clientRect.height : getGridHeight(this.layout, this.rowHeight, this.gap)); + this._gridItemsRenderData = layoutToRenderItems(this.config, clientRect.width, this.gridCurrentHeight); } render() { - if (this.rowHeight === 'fit') { - this.renderer.setStyle(this.elementRef.nativeElement, 'height', `100%`); - - } else { - this.renderer.setStyle(this.elementRef.nativeElement, 'height', `${this._height}px`); - - } + this.renderer.setStyle(this.elementRef.nativeElement, 'height', `${this.gridCurrentHeight}px`); this.updateGridItemsStyles(); } @@ -404,9 +410,6 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte } this.createPlaceholderElement(placeholderClientRect, gridItem.placeholder); - const rowHeight = this.rowHeight === 'fit' ? (gridElemClientRect.height / getGridHeight(this.layout, 1, this.gap)) : this.rowHeight; - - let newLayout: KtdGridLayoutItem[]; // TODO (enhancement): consider move this 'side effect' observable inside the main drag loop. @@ -453,7 +456,8 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte const {layout, draggedItemPos} = calcNewStateFunc(gridItem, { layout: currentLayout, - rowHeight, + rowHeight: this.rowHeight, + height: this.height, cols: this.cols, preventCollision: this.preventCollision, gap: this.gap, @@ -466,11 +470,12 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte }); newLayout = layout; - this._height = getGridHeight(newLayout, rowHeight, this.gap); + this.gridCurrentHeight = this.height ?? (this.rowHeight === 'fit' ? gridElemClientRect.height : getGridHeight(newLayout, this.rowHeight, this.gap)) this._gridItemsRenderData = layoutToRenderItems({ cols: this.cols, - rowHeight, + rowHeight: this.rowHeight, + height: this.height, layout: newLayout, preventCollision: this.preventCollision, gap: this.gap, diff --git a/projects/angular-grid-layout/src/lib/grid.definitions.ts b/projects/angular-grid-layout/src/lib/grid.definitions.ts index 4470201..1bfa3fa 100644 --- a/projects/angular-grid-layout/src/lib/grid.definitions.ts +++ b/projects/angular-grid-layout/src/lib/grid.definitions.ts @@ -18,7 +18,8 @@ export type KtdGridCompactType = CompactType; export interface KtdGridCfg { cols: number; - rowHeight: number; // row height in pixels + rowHeight: number | 'fit'; // row height in pixels + height: number | null; layout: KtdGridLayoutItem[]; preventCollision: boolean; gap: number; diff --git a/projects/angular-grid-layout/src/lib/utils/grid.utils.ts b/projects/angular-grid-layout/src/lib/utils/grid.utils.ts index 11698e2..e91e4aa 100644 --- a/projects/angular-grid-layout/src/lib/utils/grid.utils.ts +++ b/projects/angular-grid-layout/src/lib/utils/grid.utils.ts @@ -11,6 +11,14 @@ export function ktdTrackById(index: number, item: {id: string}) { return item.id; } +/** Given a layout, the gridHeight and the gap return the resulting rowHeight */ +export function ktdGetGridItemRowHeight(layout: KtdGridLayout, gridHeight: number, gap: number): number { + const numberOfRows = layout.reduce((acc, cur) => Math.max(acc, Math.max(cur.y + cur.h, 0)), 0); + const gapTotalHeight = (numberOfRows - 1) * gap; + const gridHeightMinusGap = gridHeight - gapTotalHeight; + return gridHeightMinusGap / numberOfRows; +} + /** * Call react-grid-layout utils 'compact()' function and return the compacted layout. * @param layout to be compacted. @@ -95,11 +103,15 @@ export function ktdGridItemDragging(gridItem: KtdGridItemComponent, config: KtdG const gridRelXPos = clientX - gridElementLeftPosition - offsetX; const gridRelYPos = clientY - gridElementTopPosition - offsetY; + const rowHeightInPixels = config.rowHeight === 'fit' + ? ktdGetGridItemRowHeight(config.layout, config.height ?? gridElemClientRect.height, config.gap) + : config.rowHeight; + // Get layout item position const layoutItem: KtdGridLayoutItem = { ...draggingElemPrevItem, x: screenXToGridX(gridRelXPos , config.cols, gridElemClientRect.width, config.gap), - y: screenYToGridY(gridRelYPos, config.rowHeight, gridElemClientRect.height, config.gap) + y: screenYToGridY(gridRelYPos, rowHeightInPixels, gridElemClientRect.height, config.gap) }; // Correct the values if they overflow, since 'moveElement' function doesn't do it @@ -161,11 +173,15 @@ export function ktdGridItemResizing(gridItem: KtdGridItemComponent, config: KtdG const width = clientX + resizeElemOffsetX - (dragElemClientRect.left + scrollDifference.left); const height = clientY + resizeElemOffsetY - (dragElemClientRect.top + scrollDifference.top); + const rowHeightInPixels = config.rowHeight === 'fit' + ? ktdGetGridItemRowHeight(config.layout, config.height ?? gridElemClientRect.height, config.gap) + : config.rowHeight; + // Get layout item grid position const layoutItem: KtdGridLayoutItem = { ...draggingElemPrevItem, w: screenWidthToGridWidth(width, config.cols, gridElemClientRect.width, config.gap), - h: screenHeightToGridHeight(height, config.rowHeight, gridElemClientRect.height, config.gap) + h: screenHeightToGridHeight(height, rowHeightInPixels, gridElemClientRect.height, config.gap) }; layoutItem.w = limitNumberWithinRange(layoutItem.w, gridItem.minW ?? layoutItem.minW, gridItem.maxW ?? layoutItem.maxW); diff --git a/projects/demo-app/src/app/app-routing.module.ts b/projects/demo-app/src/app/app-routing.module.ts index 43e605c..43e5372 100644 --- a/projects/demo-app/src/app/app-routing.module.ts +++ b/projects/demo-app/src/app/app-routing.module.ts @@ -26,6 +26,11 @@ const routes: Routes = [ redirectTo: 'scroll-test', pathMatch: 'full' }, + { + path: 'row-height-fit', + loadComponent: () => import('./row-height-fit/row-height-fit.component').then(m => m.KtdRowHeightFitComponent), + data: {title: 'Angular Grid Layout - Row Height Fit'} + }, { path: '**', redirectTo: 'playground' diff --git a/projects/demo-app/src/app/app.component.scss b/projects/demo-app/src/app/app.component.scss index 97a36a3..e551f11 100644 --- a/projects/demo-app/src/app/app.component.scss +++ b/projects/demo-app/src/app/app.component.scss @@ -1,5 +1,6 @@ :host { width: 100%; + height: 100%; display: block; background-color: var(--ktd-background-color-step-50); box-sizing: border-box; diff --git a/projects/demo-app/src/app/custom-handles/custom-handles.component.html b/projects/demo-app/src/app/custom-handles/custom-handles.component.html index 9698e3e..cfbaf1e 100644 --- a/projects/demo-app/src/app/custom-handles/custom-handles.component.html +++ b/projects/demo-app/src/app/custom-handles/custom-handles.component.html @@ -25,4 +25,5 @@

Other examples:

Playground Real life example Scroll test + Row Height Fit diff --git a/projects/demo-app/src/app/playground/playground.component.html b/projects/demo-app/src/app/playground/playground.component.html index 609dcea..3df57df 100644 --- a/projects/demo-app/src/app/playground/playground.component.html +++ b/projects/demo-app/src/app/playground/playground.component.html @@ -16,9 +16,14 @@ Columns - + Row height - + + Fit + + + Grid height + Drag Threshold @@ -78,10 +83,10 @@ - diff --git a/projects/demo-app/src/app/playground/playground.component.ts b/projects/demo-app/src/app/playground/playground.component.ts index ad36d8a..5dcadd8 100644 --- a/projects/demo-app/src/app/playground/playground.component.ts +++ b/projects/demo-app/src/app/playground/playground.component.ts @@ -8,6 +8,7 @@ import { import { ktdArrayRemoveItem } from '../utils'; import { DOCUMENT } from '@angular/common'; import { coerceNumberProperty } from '@angular/cdk/coercion'; +import { MatCheckboxChange } from '@angular/material/checkbox'; @Component({ selector: 'ktd-playground', @@ -20,6 +21,8 @@ export class KtdPlaygroundComponent implements OnInit, OnDestroy { cols = 12; rowHeight = 50; + rowHeightFit = false; + gridHeight: null | number = null; compactType: 'vertical' | 'horizontal' | null = 'vertical'; layout: KtdGridLayout = [ {id: '0', x: 5, y: 0, w: 2, h: 3}, @@ -154,6 +157,14 @@ export class KtdPlaygroundComponent implements OnInit, OnDestroy { this.rowHeight = coerceNumberProperty((event.target as HTMLInputElement).value); } + onRowHeightFitChange(change: MatCheckboxChange) { + this.rowHeightFit = change.checked; + } + + onGridHeightChange(event: Event) { + this.gridHeight = coerceNumberProperty((event.target as HTMLInputElement).value); + } + onDragStartThresholdChange(event: Event) { this.dragStartThreshold = coerceNumberProperty((event.target as HTMLInputElement).value); } diff --git a/projects/demo-app/src/app/real-life-example/real-life-example.component.html b/projects/demo-app/src/app/real-life-example/real-life-example.component.html index 2262134..ed2a1a2 100644 --- a/projects/demo-app/src/app/real-life-example/real-life-example.component.html +++ b/projects/demo-app/src/app/real-life-example/real-life-example.component.html @@ -154,6 +154,7 @@

Other examples:

Playground Custom handles Scroll test + Row Height Fit diff --git a/projects/demo-app/src/app/row-height-fit/row-height-fit.component.html b/projects/demo-app/src/app/row-height-fit/row-height-fit.component.html new file mode 100644 index 0000000..dabb133 --- /dev/null +++ b/projects/demo-app/src/app/row-height-fit/row-height-fit.component.html @@ -0,0 +1,31 @@ +
+ + +
{{item.id}}
+
+
+
+
+
+ + diff --git a/projects/demo-app/src/app/row-height-fit/row-height-fit.component.scss b/projects/demo-app/src/app/row-height-fit/row-height-fit.component.scss new file mode 100644 index 0000000..f3ff844 --- /dev/null +++ b/projects/demo-app/src/app/row-height-fit/row-height-fit.component.scss @@ -0,0 +1,67 @@ +:host { + width: 100%; + height: calc(100% - 65px); + padding: 12px 12px; + box-sizing: border-box; + + display: flex; + flex-direction: column; + + + .grid-container { + flex: 1; + min-height: 0; // Let it shrink + box-sizing: border-box; + border: 1px solid var(--ktd-border-color); + background-color: var(--ktd-background-color); + border-radius: 2px; + } + + ktd-grid-item { + color: #121212; + } + + ktd-grid { + transition: height 500ms ease; + } + + .grid-item-content { + box-sizing: border-box; + background: #ccc; + border: 1px solid; + width: 100%; + height: 100%; + user-select: none; + display: flex; + align-items: center; + justify-content: center; + } + + .grid-item-remove-handle { + position: absolute; + cursor: pointer; + display: flex; + justify-content: center; + width: 20px; + height: 20px; + top: 0; + right: 0; + + &::after { + content: 'x'; + color: #121212; + font-size: 16px; + font-weight: 300; + font-family: Arial, sans-serif; + } + } + + // Add custom drag mouse styles + ktd-grid-item { + cursor: grab; + + &.ktd-grid-item-dragging { + cursor: grabbing; + } + } +} diff --git a/projects/demo-app/src/app/row-height-fit/row-height-fit.component.ts b/projects/demo-app/src/app/row-height-fit/row-height-fit.component.ts new file mode 100644 index 0000000..33ce2c0 --- /dev/null +++ b/projects/demo-app/src/app/row-height-fit/row-height-fit.component.ts @@ -0,0 +1,84 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { KtdGridModule, KtdGridComponent, KtdGridLayout, ktdTrackById } from '@katoid/angular-grid-layout'; +import { fromEvent, merge, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { ktdArrayRemoveItem } from '../utils'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'ktd-row-height-fit', + standalone: true, + imports: [CommonModule, KtdGridModule, RouterModule], + templateUrl: './row-height-fit.component.html', + styleUrls: ['./row-height-fit.component.scss'] +}) +export class KtdRowHeightFitComponent implements OnInit { + @ViewChild(KtdGridComponent, {static: true}) grid: KtdGridComponent; + @ViewChild('gridContainer', {static: true}) gridContainerElementRef: ElementRef; + trackById = ktdTrackById; + + cols = 12; + gridHeight: null | number = 500; + compactType: 'vertical' | 'horizontal' | null = 'vertical'; + layout: KtdGridLayout = [ + {id: '0', x: 5, y: 0, w: 2, h: 3}, + {id: '1', x: 2, y: 2, w: 1, h: 2}, + {id: '2', x: 3, y: 7, w: 1, h: 2}, + {id: '3', x: 2, y: 0, w: 3, h: 2}, + {id: '4', x: 5, y: 3, w: 2, h: 3}, + {id: '5', x: 0, y: 4, w: 1, h: 3}, + {id: '6', x: 9, y: 0, w: 3, h: 4}, + {id: '7', x: 9, y: 4, w: 2, h: 2}, + {id: '8', x: 3, y: 2, w: 2, h: 5}, + {id: '9', x: 7, y: 0, w: 1, h: 3}, + {id: '10', x: 2, y: 4, w: 1, h: 4}, + {id: '11', x: 0, y: 0, w: 2, h: 4}, + {id: '12', x: 7, y: 3, w: 2, h: 2}, + {id: '13', x: 8, y: 5, w: 1, h: 4}, + {id: '14', x: 9, y: 6, w: 3, h: 3} + ]; + dragStartThreshold = 0; + gap = 0; + disableDrag = false; + disableResize = false; + disableRemove = false; + preventCollision = false; + resizeSubscription: Subscription; + + constructor() { } + + ngOnInit() { + this.gridHeight = this.gridContainerElementRef.nativeElement.getBoundingClientRect().height; + + this.resizeSubscription = merge( + fromEvent(window, 'resize'), + fromEvent(window, 'orientationchange') + ).pipe( + debounceTime(50), + ).subscribe(() => { + const newHeight = this.gridContainerElementRef.nativeElement.getBoundingClientRect().height; + if (this.gridHeight !== newHeight) { + this.gridHeight = this.gridContainerElementRef.nativeElement.getBoundingClientRect().height; + } else { // If grid height is the same, resize ii in case only the width has changed. + this.grid.resize(); + } + }); + } + + /** + * Fired when a mousedown happens on the remove grid item button. + * Stops the event from propagating an causing the drag to start. + * We don't want to drag when mousedown is fired on remove icon button. + */ + stopEventPropagation(event: Event) { + event.preventDefault(); + event.stopPropagation(); + } + + /** Removes the item from the layout */ + removeItem(id: string) { + // Important: Don't mutate the array. Let Angular know that the layout has changed creating a new reference. + this.layout = ktdArrayRemoveItem(this.layout, (item) => item.id === id); + } +} diff --git a/projects/demo-app/src/app/scroll-test/scroll-test.component.html b/projects/demo-app/src/app/scroll-test/scroll-test.component.html index 459bbd9..276cd78 100644 --- a/projects/demo-app/src/app/scroll-test/scroll-test.component.html +++ b/projects/demo-app/src/app/scroll-test/scroll-test.component.html @@ -44,5 +44,6 @@

Other examples:

Playground Custom handles Real life example + Row Height Fit