New FlexMasonry component implements a masonry layout using flexbox
This commit is contained in:
parent
d9083fdde6
commit
f27895c4c8
66
ui/app/components/flex-masonry.js
Normal file
66
ui/app/components/flex-masonry.js
Normal 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';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
37
ui/app/styles/components/flex-masonry.scss
Normal file
37
ui/app/styles/components/flex-masonry.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
ui/app/templates/components/flex-masonry.hbs
Normal file
11
ui/app/templates/components/flex-masonry.hbs
Normal 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>
|
Loading…
Reference in a new issue