feat: refactor waterfall to be more efficient

This commit is contained in:
Zephyrrus 2020-07-07 02:50:03 +03:00
parent fb0bc57542
commit 5d61b4d000
4 changed files with 127 additions and 250 deletions

View File

@ -56,6 +56,7 @@
"imagesloaded": "^4.1.4",
"jsonwebtoken": "^8.5.0",
"knex": "^0.16.3",
"masonry-layout": "^4.2.2",
"moment": "^2.24.0",
"multer": "^1.4.1",
"mysql": "^2.16.0",
@ -117,7 +118,28 @@
],
"class-methods-use-this": "off",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-plusplus": [
"error",
{
"allowForLoopAfterthoughts": true
}
],
"no-underscore-dangle": [
"error",
{
"allow": [
"_id"
]
}
],
"import/extensions": [
"error",
"always",
{
"js": "never",
"ts": "never"
}
],
"vue/attribute-hyphenation": 0,
"vue/html-closing-bracket-newline": [
"error",

View File

@ -25,6 +25,7 @@
v-if="showWaterfall"
:gutterWidth="10"
:gutterHeight="4"
:options="{fitWidth: true}"
:itemWidth="width"
:items="gridFiles">
<template v-slot="{item}">
@ -451,6 +452,10 @@ div.actions {
display: none;
}
.waterfall {
margin: 0 auto;
}
.waterfall-item:hover {
div.actions {
opacity: 1;

View File

@ -1,51 +1,20 @@
<template>
<div class="waterfall">
<WaterfallItem v-for="(item, index) in items" :key="item.id" :ref="`item-${item.id}`" :width="itemWidth">
<div ref="waterfall" class="waterfall">
<WaterfallItem
v-for="(item, index) in items"
:key="item.id"
:style="{ width: `${itemWidth}px`, marginBottom: `${gutterHeight}px` }"
:width="itemWidth">
<slot :item="item" />
</WaterfallItem>
</div>
</template>
<script>
import WaterfallItem from './WaterfallItem.vue';
const quickSort = (arr, type) => {
const left = [];
const right = [];
if (arr.length <= 1) {
return arr;
}
const povis = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i][type] < povis[type]) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left, type).concat(povis, quickSort(right, type));
};
const getMinIndex = (arr) => {
let pos = 0;
for (let i = 0; i < arr.length; i++) {
if (arr[pos] > arr[i]) {
pos = i;
}
}
return pos;
};
const _ = {
on(el, type, func, capture = false) {
el.addEventListener(type, func, capture);
},
off(el, type, func, capture = false) {
el.removeEventListener(type, func, capture);
},
};
const sum = (arr) => arr.reduce((acc, val) => acc + val, 0);
const isBrowser = typeof window !== 'undefined';
const Masonry = isBrowser ? window.Masonry || require('masonry-layout') : null;
const imagesloaded = isBrowser ? require('imagesloaded') : null;
export default {
name: 'Waterfall',
@ -53,159 +22,112 @@ export default {
WaterfallItem,
},
props: {
gutterWidth: {
type: Number,
default: 0,
},
gutterHeight: {
type: Number,
default: 0,
},
resizable: {
type: Boolean,
default: true,
},
align: {
type: String,
default: 'center',
},
fixWidth: {
type: Number,
default: 0,
},
minCol: {
type: Number,
default: 1,
},
maxCol: {
type: Number,
default: 0,
},
percent: {
type: Array,
default: null,
},
itemWidth: {
type: Number,
default: 150,
options: {
type: Object,
default: () => {},
},
items: {
type: Array,
default: () => [],
},
},
data() {
return {
timer: null,
colNum: 0,
lastWidth: 0,
percentWidthArr: [],
readyChildCount: 0,
};
},
watch: {
items() {
this.$nextTick(() => this.render('watch'));
itemWidth: {
type: Number,
default: 150,
},
gutterWidth: {
type: Number,
default: 10,
},
gutterHeight: {
type: Number,
default: 4,
},
},
created() {
this.$on('itemRender', () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.render('created');
}, 0);
});
},
mounted() {
this.resizeHandle();
this.$watch('resizable', this.resizeHandle);
this.initializeMasonry();
this.imagesLoaded();
},
beforeDestroy() {
this.$off('itemRender');
_.off(window, 'resize', this.render);
updated() {
this.performLayout();
this.imagesLoaded();
},
unmounted() {
this.masonry.destroy();
},
methods: {
calulate(arr) {
const pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth;
//
if (this.percent) {
this.colNum = this.percent.length;
const total = sum(this.percent);
this.percentWidthArr = this.percent.map((value) => (value / total) * pageWidth);
this.lastWidth = 0;
//
} else {
this.colNum = parseInt(pageWidth / (arr.width + this.gutterWidth), 10);
if (this.minCol && this.colNum < this.minCol) {
this.colNum = this.minCol;
this.lastWidth = 0;
} else if (this.maxCol && this.colNum > this.maxCol) {
this.colNum = this.maxCol;
this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
} else {
this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
}
imagesLoaded() {
const node = this.$refs.waterfall;
imagesloaded(
node,
() => {
this.masonry.layout();
},
);
},
performLayout() {
const diff = this.diffDomChildren();
if (diff.removed.length > 0) {
this.masonry.remove(diff.removed);
this.masonry.reloadItems();
}
if (diff.appended.length > 0) {
this.masonry.appended(diff.appended);
this.masonry.reloadItems();
}
if (diff.prepended.length > 0) {
this.masonry.prepended(diff.prepended);
}
if (diff.moved.length > 0) {
this.masonry.reloadItems();
}
this.masonry.layout();
},
diffDomChildren() {
const oldChildren = this.domChildren.filter((element) => !!element.parentNode);
const newChildren = this.getNewDomChildren();
const removed = oldChildren.filter((oldChild) => !newChildren.includes(oldChild));
const domDiff = newChildren.filter((newChild) => !oldChildren.includes(newChild));
const prepended = domDiff.filter((newChild, index) => newChildren[index] === newChild);
const appended = domDiff.filter((el) => !prepended.includes(el));
let moved = [];
if (removed.length === 0) {
moved = oldChildren.filter((child, index) => index !== newChildren.indexOf(child));
}
this.domChildren = newChildren;
return {
old: oldChildren,
new: newChildren,
removed,
appended,
prepended,
moved,
};
},
initializeMasonry() {
if (!this.masonry) {
this.masonry = new Masonry(
this.$refs.waterfall,
{
columnWidth: this.itemWidth,
gutter: this.gutterWidth,
...this.options,
},
);
this.domChildren = this.getNewDomChildren();
}
},
resizeHandle() {
if (this.resizable) {
_.on(window, 'resize', this.render, false);
} else {
_.off(window, 'resize', this.render, false);
}
},
render(context) {
console.log(context);
if (!this.items) return;
//
let childArr = [];
childArr = this.items.map(({ id }) => this.$refs[`item-${id}`][0].getMeta());
childArr = quickSort(childArr, 'order');
//
this.calulate(childArr[0]);
const offsetArr = Array(this.colNum).fill(0);
//
childArr.forEach((child) => {
const position = getMinIndex(offsetArr);
//
if (this.percent) {
let left = 0;
child.el.style.width = `${this.percentWidthArr[position]}px`;
if (position === 0) {
left = 0;
} else {
for (let i = 0; i < position; i++) {
left += this.percentWidthArr[i];
}
}
child.el.style.left = `${left}px`;
//
} else {
if (this.align === 'left') { // eslint-disable-line no-lonely-if
child.el.style.left = `${position * (child.width + this.gutterWidth)}px`;
} else if (this.align === 'right') {
child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth}px`;
} else {
child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth / 2}px`;
}
}
if (child.height === 0) {
return;
}
child.el.style.top = `${offsetArr[position]}px`;
offsetArr[position] += child.height + this.gutterHeight;
this.$el.style.height = `${Math.max(...offsetArr)}px`;
});
this.$emit('rendered', this);
getNewDomChildren() {
const node = this.$refs.waterfall;
const children = this.options && this.options.itemSelector
? node.querySelectorAll(this.options.itemSelector) : node.children;
return Array.prototype.slice.call(children);
},
},
};
</script>
<style>
.waterfall {
position: relative;
}
<style lang="scss" scoped>
.wfi {
}
</style>

View File

@ -3,80 +3,8 @@
<slot />
</div>
</template>
<script>
import imagesLoaded from 'imagesloaded';
export default {
name: 'WaterfallItem',
props: {
order: {
type: Number,
default: 0,
},
width: {
type: Number,
default: 150,
},
video: {
type: Boolean,
default: false,
},
},
data() {
return {
itemWidth: 0,
height: 0,
};
},
created() {
this.$watch(() => this.height, this.emit);
},
mounted() {
this.$el.style.display = 'none';
this.$el.style.width = `${this.width}px`;
this.emit();
if (this.video) {
// find first video object
const videoEl = this.$slots.default.find((e) => e.tag?.toLowerCase() === 'video');
const el = videoEl.elm;
// add event listener for video loaded
el.onloadeddata = () => {
this.$el.style.left = '-9999px';
this.$el.style.top = '-9999px';
this.$el.style.display = 'block';
this.height = el.offsetHeight + 5;
this.itemWidth = el.offsetWidth;
};
} else {
imagesLoaded(this.$el, () => {
this.$el.style.left = '-9999px';
this.$el.style.top = '-9999px';
this.$el.style.display = 'block';
this.height = this.$el.offsetHeight;
this.itemWidth = this.$el.offsetWidth;
});
}
},
methods: {
emit() {
this.$parent.$emit('itemRender');
},
getMeta() {
return {
el: this.$el,
height: this.height,
width: this.itemWidth,
order: this.order,
};
},
},
};
</script>
<style scoped>
.waterfall-item {
position: absolute;
}
</style>