Skip to content

Commit b5624ea

Browse files
authored
refactor: implement masonry layout directive and update quick-create component for improved card display (#610)
1 parent 278121f commit b5624ea

File tree

6 files changed

+110
-73
lines changed

6 files changed

+110
-73
lines changed

package-lock.json

Lines changed: 0 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/_samples/mediaco/components/banner/banner.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
display: grid;
33
grid-template-columns: repeat(2, minmax(0, 1fr));
44
column-gap: calc(2 * 0.5rem);
5-
row-gap: 1.5rem;
5+
gap: 1.5rem;
66
align-items: start;
77
}
88

src/app/_samples/mediaco/components/quick-create/quick-create.component.html

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,10 @@ <h3>Get started</h3>
66
<div class="active-line"></div>
77
</div>
88
</div>
9-
<div class="masonry-wrapper">
10-
<!-- Left Column -->
11-
<div class="masonry-column">
12-
<ng-container *ngFor="let card of cases$; let i = index">
13-
<ng-container *ngIf="i % 2 === 0">
14-
<ng-container *ngTemplateOutlet="cardTemplate; context: { $implicit: card }"></ng-container>
15-
</ng-container>
16-
</ng-container>
17-
</div>
18-
19-
<!-- Right Column -->
20-
<div class="masonry-column">
21-
<ng-container *ngFor="let card of cases$; let i = index">
22-
<ng-container *ngIf="i % 2 === 1">
23-
<ng-container *ngTemplateOutlet="cardTemplate; context: { $implicit: card }"></ng-container>
24-
</ng-container>
25-
</ng-container>
26-
</div>
9+
<div class="masonry-wrapper" appMasonry [masonryGap]="16" [masonryRowHeight]="1" masonryItemSelector=".card">
10+
<ng-container *ngFor="let card of cases$; let i = index">
11+
<ng-container *ngTemplateOutlet="cardTemplate; context: { $implicit: card }"></ng-container>
12+
</ng-container>
2713
</div>
2814
</div>
2915
</div>

src/app/_samples/mediaco/components/quick-create/quick-create.component.scss

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,20 @@ $text-dark: #2c2c2c;
4343
}
4444

4545
.masonry-wrapper {
46-
display: flex;
47-
flex-direction: column;
46+
display: grid;
47+
grid-template-columns: 1fr;
48+
grid-auto-rows: 1px;
4849
gap: $gap-size;
4950
width: 100%;
5051
max-width: 1100px;
52+
align-items: start;
53+
grid-auto-flow: row dense;
5154

5255
@media (min-width: 768px) {
53-
flex-direction: row;
54-
align-items: flex-start;
56+
grid-template-columns: repeat(2, 1fr);
5557
}
5658
}
5759

58-
.masonry-column {
59-
display: flex;
60-
flex-direction: column;
61-
gap: $gap-size;
62-
flex: 1;
63-
width: 100%;
64-
}
65-
6660
// --- Card Styles ---
6761
.card {
6862
background: #fff;
@@ -74,6 +68,10 @@ $text-dark: #2c2c2c;
7468
overflow: hidden;
7569
display: flex;
7670
flex-direction: column;
71+
height: fit-content;
72+
73+
// Dynamic grid-row-end will be set via JavaScript for true masonry effect
74+
grid-row-end: span var(--row-span, auto);
7775

7876
&:hover {
7977
.icon-box {

src/app/_samples/mediaco/components/quick-create/quick-create.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
33
import { MatIcon } from '@angular/material/icon';
44
import { Utils } from '@pega/angular-sdk-components';
55
import { QUICK_LINKS_DATA } from './quick-create.utils';
6+
import { MasonryDirective } from '../../directives/masonry.directive';
67

78
interface QuickCreateProps {
89
// If any, enter additional props that only exist on this component
@@ -15,7 +16,7 @@ interface QuickCreateProps {
1516
selector: 'app-quick-create',
1617
templateUrl: './quick-create.component.html',
1718
styleUrls: ['./quick-create.component.scss'],
18-
imports: [CommonModule, MatIcon]
19+
imports: [CommonModule, MatIcon, MasonryDirective]
1920
})
2021
export class QuickCreateComponent implements OnInit, OnChanges {
2122
@Input() pConn$: typeof PConnect;
@@ -27,6 +28,7 @@ export class QuickCreateComponent implements OnInit, OnChanges {
2728
showCaseIcons$?: boolean;
2829
classFilter$: any;
2930
cases$: any = [];
31+
3032
constructor(private utils: Utils) {}
3133

3234
ngOnInit() {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Directive, ElementRef, AfterViewInit, OnDestroy, Input, HostListener } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[appMasonry]',
5+
standalone: true
6+
})
7+
export class MasonryDirective implements AfterViewInit, OnDestroy {
8+
@Input() masonryGap: number = 16;
9+
@Input() masonryRowHeight: number = 1;
10+
@Input() masonryItemSelector: string = '.card';
11+
12+
private resizeTimeout: any;
13+
private mutationObserver: MutationObserver | null = null;
14+
15+
constructor(private elementRef: ElementRef) {}
16+
17+
ngAfterViewInit() {
18+
// Apply masonry layout after view is initialized
19+
setTimeout(() => {
20+
this.applyMasonryLayout();
21+
}, 100);
22+
23+
// Watch for DOM changes (new items added/removed)
24+
this.setupMutationObserver();
25+
}
26+
27+
ngOnDestroy() {
28+
if (this.resizeTimeout) {
29+
clearTimeout(this.resizeTimeout);
30+
}
31+
if (this.mutationObserver) {
32+
this.mutationObserver.disconnect();
33+
}
34+
}
35+
36+
@HostListener('window:resize', ['$event'])
37+
onWindowResize() {
38+
// Throttle resize events to avoid excessive recalculations
39+
if (this.resizeTimeout) {
40+
clearTimeout(this.resizeTimeout);
41+
}
42+
43+
this.resizeTimeout = setTimeout(() => {
44+
this.applyMasonryLayout();
45+
}, 150);
46+
}
47+
48+
private setupMutationObserver() {
49+
this.mutationObserver = new MutationObserver(() => {
50+
// Debounce DOM changes
51+
if (this.resizeTimeout) {
52+
clearTimeout(this.resizeTimeout);
53+
}
54+
55+
this.resizeTimeout = setTimeout(() => {
56+
this.applyMasonryLayout();
57+
}, 100);
58+
});
59+
60+
this.mutationObserver.observe(this.elementRef.nativeElement, {
61+
childList: true,
62+
subtree: true
63+
});
64+
}
65+
66+
private applyMasonryLayout() {
67+
const grid = this.elementRef.nativeElement;
68+
if (!grid) return;
69+
70+
const items = grid.querySelectorAll(this.masonryItemSelector);
71+
if (items.length === 0) return;
72+
73+
// Reset any existing row spans first
74+
items.forEach((item: HTMLElement) => {
75+
item.style.removeProperty('--row-span');
76+
});
77+
78+
// Wait for layout to settle after reset, then recalculate
79+
requestAnimationFrame(() => {
80+
items.forEach((item: HTMLElement) => {
81+
const itemHeight = item.getBoundingClientRect().height;
82+
const rowSpan = Math.ceil((itemHeight + this.masonryGap) / (this.masonryRowHeight + this.masonryGap));
83+
item.style.setProperty('--row-span', rowSpan.toString());
84+
});
85+
});
86+
}
87+
88+
// Public method to manually trigger layout recalculation
89+
public recalculateLayout() {
90+
this.applyMasonryLayout();
91+
}
92+
}

0 commit comments

Comments
 (0)