@offensichtbar-codestock/ngx-flex-masonry-grid
Version:
Angular Module for displaying items in a flex-based masonry layout without any third party dependencies
405 lines (393 loc) • 15.7 kB
JavaScript
import { ɵɵdefineInjectable, Injectable, InjectionToken, Component, ElementRef, Inject, HostBinding, HostListener, EventEmitter, Output, ContentChildren, NgModule } from '@angular/core';
import { animation, animate, style, trigger, state, transition, useAnimation, AnimationBuilder } from '@angular/animations';
import { Subject } from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
class NgxFlexMasonryGridService {
constructor() {
this._imageobserved = new Subject();
this.imageobserved = this._imageobserved.asObservable();
this._layoutshouldbeupdated = new Subject();
this.layoutshouldbeupdated = this._layoutshouldbeupdated.asObservable();
this._itemremoved = new Subject();
this.itemremoved = this._itemremoved.asObservable();
this._allitemsloaded = new Subject();
this.allitemsloaded = this._allitemsloaded.asObservable();
this._loaded_items = [];
}
get loadeditems() {
return this._loaded_items;
}
observeimage(values) {
const itemwithmaxheight = values.reduce((max, obj) => (max.height > obj.height) ? max : obj);
this._imageobserved.next(itemwithmaxheight);
}
addItem(item, countallitems) {
this._loaded_items = [...this._loaded_items, item];
this._layoutshouldbeupdated.next(item);
if (this._loaded_items.length === countallitems) {
this._allitemsloaded.next();
}
}
removeItem(item) {
const index = this._loaded_items.findIndex(griditem => Object.is(griditem, item));
this._itemremoved.next(this._loaded_items[index]);
this._loaded_items = [
...this._loaded_items.slice(0, index),
...this._loaded_items.slice(index + 1, this._loaded_items.length)
];
}
clearStack() {
this._loaded_items = [];
}
}
NgxFlexMasonryGridService.ɵprov = ɵɵdefineInjectable({ factory: function NgxFlexMasonryGridService_Factory() { return new NgxFlexMasonryGridService(); }, token: NgxFlexMasonryGridService, providedIn: "root" });
NgxFlexMasonryGridService.decorators = [
{ type: Injectable, args: [{
providedIn: 'root'
},] }
];
NgxFlexMasonryGridService.ctorParameters = () => [];
const CIRCULAR_IMPORT_PARENT = new InjectionToken('CIRCULAR_IMPORT_PARENT');
const timing = '.4s ease-out';
class NgxFMG_ANIMATION {
static get UPDATE_POSITION() {
return animation([
animate(timing, style({ transform: 'translateY({{ animatePosY }})' })),
]);
}
static get FADE_IN() {
return animation([
style({ opacity: 0, visibility: 'visible' }),
animate(timing, style({ opacity: 1 })),
]);
}
static get TRIGGER_FADE_OUT() {
return trigger('TRIGGER_FADE_OUT', [
state(':leave', style({ opacity: 1 })),
transition('* => void', animate(timing, style({ opacity: 0 })))
]);
}
}
class NgxFlexMasonryGridItemComponent {
constructor(element, builder, service, parent) {
this.element = element;
this.builder = builder;
this.service = service;
this.parent = parent;
this.heightprops = 'max-content';
this._translateY = 0;
this._isready = false;
this._remove = false;
}
get getLeaveDrawer() {
return this._remove;
}
animationIsDone() {
if (this._remove)
this.service.removeItem(this);
}
get height() {
return this.element.nativeElement.offsetHeight;
}
get isready() {
return this._isready;
}
set isready(ready) {
this._isready = ready;
}
get width() {
const marginLeft = window
.getComputedStyle(this.element.nativeElement, null)
.getPropertyValue('margin-left')
.match(/\d+/);
const marginRight = window
.getComputedStyle(this.element.nativeElement, null)
.getPropertyValue('margin-right')
.match(/\d+/);
return this.element.nativeElement.offsetWidth + (parseInt(marginLeft[0]) + parseInt(marginRight[0]));
}
set translateY(value) {
if (this._isready) {
this.upDatePosition(value);
}
else {
this.element.nativeElement.style.transform = `translateY(-${value}px)`;
}
this._translateY = value;
}
get translateY() {
return this._translateY;
}
ngOnInit() { }
upDatePosition(to) {
const params = { animatePosY: `-${to}px` };
const metadata = useAnimation(NgxFMG_ANIMATION.UPDATE_POSITION, { params: params });
const player = this.builder.build(metadata).create(this.element.nativeElement);
player.play();
}
playAnimation() {
const metadata = NgxFMG_ANIMATION.FADE_IN;
this.element.nativeElement.style.visibility = 'visible';
const player = this.builder.build(metadata).create(this.element.nativeElement);
player.play();
}
ngAfterViewInit() {
this.translateY = 0;
this.startLoading();
}
startLoading() {
const images = this.element.nativeElement.querySelectorAll('img');
this.images = new Set(images);
let loaded = [];
if (images.length == 0) {
setTimeout(() => {
this.service.observeimage([...loaded, {
item: this,
height: this.element.nativeElement.offsetHeight
}]);
});
return;
}
Array.from(this.images).forEach((image) => {
this.loadImage(image.src).then(props => {
var _a;
loaded = this.checkActionLoaded(loaded, images, props);
(_a = this.images) === null || _a === void 0 ? void 0 : _a.delete(image);
}).catch((error) => {
var _a;
loaded = this.checkActionLoaded(loaded, images);
(_a = this.images) === null || _a === void 0 ? void 0 : _a.delete(image);
});
});
}
checkActionLoaded(loaded, images, props) {
loaded = [...loaded, (props) ? props : {
item: this,
height: this.element.nativeElement.offsetHeight
}];
if (loaded.length === images.length) {
this.service.observeimage(loaded);
}
return loaded;
}
loadImage(src) {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => resolve({
item: this,
height: this.element.nativeElement.offsetHeight
});
img.onerror = reject;
img.src = src;
});
}
ngOnDestroy() {
this._remove = true;
}
}
NgxFlexMasonryGridItemComponent.decorators = [
{ type: Component, args: [{
selector: 'osb-ngx-flexmasonry-grid-item',
template: '<ng-content></ng-content> ',
animations: [
NgxFMG_ANIMATION.TRIGGER_FADE_OUT
]
},] }
];
NgxFlexMasonryGridItemComponent.ctorParameters = () => [
{ type: ElementRef },
{ type: AnimationBuilder },
{ type: NgxFlexMasonryGridService },
{ type: undefined, decorators: [{ type: Inject, args: [CIRCULAR_IMPORT_PARENT,] }] }
];
NgxFlexMasonryGridItemComponent.propDecorators = {
heightprops: [{ type: HostBinding, args: ['style.height',] }],
getLeaveDrawer: [{ type: HostBinding, args: ['@TRIGGER_FADE_OUT',] }],
animationIsDone: [{ type: HostListener, args: ['@TRIGGER_FADE_OUT.done',] }]
};
class NgxFlexMasonryGridComponent {
constructor(_element, service) {
this._element = _element;
this.service = service;
// Outputs
this.layoutComplete = new EventEmitter();
this.itemRemoved = new EventEmitter();
this.itemLoaded = new EventEmitter();
this.itemsLoaded = new EventEmitter();
this._timeoutID = 0;
this._cols = 0;
this._rows = 0;
this._item_heights = [];
this._row_heights = [];
this.isAlive = true;
this.service.layoutshouldbeupdated.pipe(takeWhile(() => this.isAlive)).subscribe((item) => {
this.itemLoaded.emit(item);
item.playAnimation();
item.isready = true;
this.layout();
});
this.service.imageobserved.pipe(takeWhile(() => this.isAlive)).subscribe((param) => {
this.add(param);
});
this.service.itemremoved.pipe(takeWhile(() => this.isAlive)).subscribe((item) => {
this.itemRemoved.emit(item);
this.layout();
});
this.service.allitemsloaded.pipe(takeWhile(() => this.isAlive)).subscribe(() => {
this.itemsLoaded.emit(this.items.length);
this.layout();
});
}
onResize(event) {
clearTimeout(this._timeoutID);
this._timeoutID = setTimeout(() => {
this.layout();
}, 40);
}
ngOnInit() { }
forceUpdateLayout() {
var _a, _b;
if (((_a = this.items) === null || _a === void 0 ? void 0 : _a.length) && ((_b = this.items) === null || _b === void 0 ? void 0 : _b.length) !== 0) {
let items = this.items.toArray().filter((item) => {
return item.isready;
});
if (this.service.loadeditems.length === items.length) {
this.layout();
}
}
}
layout() {
var _a;
if (!((_a = this.items) === null || _a === void 0 ? void 0 : _a.length))
return;
this._cols = Math.round(this._element.nativeElement.offsetWidth / this.items.toArray()[0].width);
this._rows = Math.ceil(this.items.length / this._cols);
this._item_heights = this.items.map(el => el.height);
this._row_heights = this.getRowHeights();
const offsets = this.getElementOffsets();
this.translateElements(offsets);
if (this.service.loadeditems.length === this.items.length) {
this.layoutComplete.emit();
}
}
add(params) {
this.service.addItem(params.item, this.items.length);
}
getRowHeights() {
let rowheights = [];
for (let row = 0; row < this._rows; row++) {
const heightgroup = this._item_heights.slice(row * this._cols, (row + 1) * this._cols); // heightgroup caches slice (slice length === cols length) of _el_heights array
const rowHeights = Math.max(...heightgroup);
rowheights.push(rowHeights);
}
return rowheights;
}
getElementOffsets() {
const el_heightgap = [...Array(this._cols)].map(e => []);
this._item_heights.forEach((height, index) => {
const current_gap = this._row_heights[Math.floor(index / this._cols)] - height;
el_heightgap[index % this._cols].push(current_gap);
});
const el_offsets = [...Array(this._cols)].map(e => []);
/**
* Accumulates element offsets (final translation values) from el_heightgap array
* Resets translation for first row by unshifting value zero to each subarray
*/
for (let gap = 0; gap < el_heightgap.length; gap++) {
let accumulation = 0; // Cache for accumulated height differences for each col
el_offsets[gap] = el_heightgap[gap].map((val) => accumulation += val); // Maps accumulated height difference values to offset array
el_offsets[gap].pop(); // Removes last value, because the last element needs to be translated by the value of its predecessor
el_offsets[gap].unshift(0); // Adds zero offsets for first item per col, because first item doesn't need to be translated
}
let elementoffsets = [];
for (let i = 0; i < this.items.length; i++) {
const iterator = i % el_offsets.length;
const counter = Math.floor(i / el_offsets.length);
elementoffsets.push(el_offsets[iterator][counter]);
}
this.setContainerHeight();
return elementoffsets;
}
setContainerHeight() {
var _a;
if (this.items.length <= 0)
return;
let containerHeight = [];
for (let col = 0; col < this._cols; col++) {
containerHeight.push([0]);
let i = 0;
while (col + this._cols * i < this.items.length) {
let currVal = containerHeight[col % this._cols];
let newVal = (_a = this.items.toArray()[col + i * this._cols]) === null || _a === void 0 ? void 0 : _a.height;
containerHeight[col % this._cols] = parseInt(currVal) + newVal;
i++;
}
}
this._element.nativeElement.style.height = `${Math.max(...containerHeight)}px`;
}
translateElements(heights) {
this.items.forEach((child, index) => {
child.translateY = heights[index];
});
}
ngAfterContentInit() { }
ngOnDestroy() {
this.isAlive = false;
this.service.clearStack();
}
}
NgxFlexMasonryGridComponent.decorators = [
{ type: Component, args: [{
selector: 'osb-ngx-flexmasonry-grid',
template: '<ng-content></ng-content> ',
providers: [
NgxFlexMasonryGridService,
{
provide: CIRCULAR_IMPORT_PARENT,
useExisting: NgxFlexMasonryGridComponent
}
],
styles: [`
:host::ng-deep > * {
visibility: hidden;
box-sizing: border-box;
backface-visibility:hidden;
}
`]
},] }
];
NgxFlexMasonryGridComponent.ctorParameters = () => [
{ type: ElementRef },
{ type: NgxFlexMasonryGridService }
];
NgxFlexMasonryGridComponent.propDecorators = {
layoutComplete: [{ type: Output }],
itemRemoved: [{ type: Output }],
itemLoaded: [{ type: Output }],
itemsLoaded: [{ type: Output }],
items: [{ type: ContentChildren, args: [NgxFlexMasonryGridItemComponent,] }],
onResize: [{ type: HostListener, args: ['window:resize', ['$event'],] }]
};
class NgxFlexMasonryGridModule {
}
NgxFlexMasonryGridModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgxFlexMasonryGridComponent, NgxFlexMasonryGridItemComponent],
imports: [
BrowserAnimationsModule
],
providers: [
NgxFlexMasonryGridService
],
exports: [NgxFlexMasonryGridComponent, NgxFlexMasonryGridItemComponent]
},] }
];
/*
* Public API Surface of ngx-flex-masonry-grid
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxFlexMasonryGridComponent, NgxFlexMasonryGridItemComponent, NgxFlexMasonryGridModule, NgxFlexMasonryGridService, CIRCULAR_IMPORT_PARENT as ɵb };
//# sourceMappingURL=offensichtbar-codestock-ngx-flex-masonry-grid.js.map