From 5d61b4d000a114857e4519be25c5834f510d450f Mon Sep 17 00:00:00 2001
From: Zephyrrus <krisztian1997@gmail.com>
Date: Tue, 7 Jul 2020 02:50:03 +0300
Subject: feat: refactor waterfall to be more efficient

---
 src/site/components/grid/waterfall/Waterfall.vue | 266 ++++++++---------------
 1 file changed, 94 insertions(+), 172 deletions(-)

(limited to 'src/site/components/grid/waterfall/Waterfall.vue')

diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue
index a83a275..79a330a 100644
--- a/src/site/components/grid/waterfall/Waterfall.vue
+++ b/src/site/components/grid/waterfall/Waterfall.vue
@@ -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,
+		options: {
+			type: Object,
+			default: () => {},
 		},
-		percent: {
+		items: {
 			type: Array,
-			default: null,
+			default: () => [],
 		},
 		itemWidth: {
 			type: Number,
 			default: 150,
 		},
-		items: {
-			type: Array,
-			default: () => [],
+		gutterWidth: {
+			type: Number,
+			default: 10,
 		},
-	},
-	data() {
-		return {
-			timer: null,
-			colNum: 0,
-			lastWidth: 0,
-			percentWidthArr: [],
-			readyChildCount: 0,
-		};
-	},
-	watch: {
-		items() {
-			this.$nextTick(() => this.render('watch'));
+		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();
+	},
+	updated() {
+		this.performLayout();
+		this.imagesLoaded();
 	},
-	beforeDestroy() {
-		this.$off('itemRender');
-		_.off(window, 'resize', this.render);
+	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();
 		},
-		resizeHandle() {
-			if (this.resizable) {
-				_.on(window, 'resize', this.render, false);
-			} else {
-				_.off(window, 'resize', this.render, false);
+		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,
+			};
 		},
-		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);
+		initializeMasonry() {
+			if (!this.masonry) {
+				this.masonry = new Masonry(
+					this.$refs.waterfall,
+					{
+						columnWidth: this.itemWidth,
+						gutter: this.gutterWidth,
+						...this.options,
+					},
+				);
+				this.domChildren = this.getNewDomChildren();
+			}
+		},
+		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>
-- 
cgit v1.2.3

