New FlexMasonry component implements a masonry layout using flexbox

This commit is contained in:
Michael Lange 2020-09-29 14:20:34 -07:00
parent d9083fdde6
commit f27895c4c8
4 changed files with 115 additions and 0 deletions

View file

@ -0,0 +1,66 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { run } from '@ember/runloop';
import { action } from '@ember/object';
import { minIndex, max } from 'd3-array';
export default class FlexMasonry extends Component {
@tracked element = null;
@action
captureElement(element) {
this.element = element;
}
@action
reflow() {
run.next(() => {
// There's nothing to do if this is single column layout
if (!this.element || this.args.columns === 1 || !this.args.columns) return;
const columns = new Array(this.args.columns).fill(null).map(() => ({
height: 0,
elements: [],
}));
const items = this.element.querySelectorAll('.flex-masonry-item');
// First pass: assign each element to a column based on the running heights of each column
for (let item of items) {
const styles = window.getComputedStyle(item);
const marginTop = parseFloat(styles.marginTop);
const marginBottom = parseFloat(styles.marginBottom);
const height = item.clientHeight;
// Pick the shortest column accounting for margins
const column = columns[minIndex(columns, c => c.height)];
// Add the new element's height to the column height
column.height += marginTop + height + marginBottom;
column.elements.push(item);
}
// Second pass: assign an order to each element based on their column and position in the column
columns
.mapBy('elements')
.flat()
.forEach((dc, index) => {
dc.style.order = index;
});
// Gaurantee column wrapping as predicted (if the first item of a column is shorter than the difference
// beteen the height of the column and the previous column, then flexbox will naturally place the first
// item at the end of the previous column).
columns.forEach((column, index) => {
const nextHeight = index < columns.length - 1 ? columns[index + 1].height : 0;
const item = column.elements.lastObject;
if (item) {
item.style.flexBasis = item.clientHeight + Math.max(0, nextHeight - column.height) + 'px';
}
});
// Set the max height of the container to the height of the tallest column
this.element.style.maxHeight = max(columns.mapBy('height')) + 1 + 'px';
});
}
}

View file

@ -12,6 +12,7 @@
@import './components/event'; @import './components/event';
@import './components/exec-button'; @import './components/exec-button';
@import './components/exec-window'; @import './components/exec-window';
@import './components/flex-masonry';
@import './components/fs-explorer'; @import './components/fs-explorer';
@import './components/global-search-container'; @import './components/global-search-container';
@import './components/global-search-dropdown'; @import './components/global-search-dropdown';

View file

@ -0,0 +1,37 @@
.flex-masonry {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: space-between;
margin-top: -0.75em;
&.flex-masonry-columns-1 > .flex-masonry-item {
width: 100%;
}
&.flex-masonry-columns-2 > .flex-masonry-item {
width: 50%;
}
&.flex-masonry-columns-3 > .flex-masonry-item {
width: 33%;
}
&.flex-masonry-columns-4 > .flex-masonry-item {
width: 25%;
}
&.with-spacing {
> .flex-masonry-item {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
&.flex-masonry-columns-2 > .flex-masonry-item {
width: calc(50% - 0.75em);
}
&.flex-masonry-columns-3 > .flex-masonry-item {
width: calc(33% - 0.75em);
}
&.flex-masonry-columns-4 > .flex-masonry-item {
width: calc(25% - 0.75em);
}
}
}

View file

@ -0,0 +1,11 @@
<div
class="flex-masonry {{if @withSpacing "with-spacing"}} flex-masonry-columns-{{@columns}}"
{{did-insert this.captureElement}}
{{did-insert this.reflow}}
{{did-update this.reflow}}>
{{#each @items as |item|}}
<div class="flex-masonry-item">
{{yield item (action this.reflow)}}
</div>
{{/each}}
</div>