UNPKG

@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
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