效果圖
GitHub源碼&Demo: https://github.com/capricorncd/zx-waterfall
waterfall.js
/**
* Created by Capricorncd.
* Date: 2018/12/14 16:00
* https://github.com/capricorncd
*/
import App from './app.js'
// 默認配置文件
const DEFAULT_OPTS = {
// 外容器
wrapper: null,
// 子元素選擇器
itemSelector: '',
// 元素間距
gutter: 20,
itemWidth: 300
}
class ZxWaterfall {
constructor (opts) {
this.opts = Object.assign({}, DEFAULT_OPTS, opts)
// 數據列表
this.list = []
this.init()
// 監聽window.onresize
App.addEvent(window, 'resize', this.resetPosition.bind(this))
}
init () {
let opts = this.opts
if (!App.isElement(opts.wrapper)) {
throw new Error(`瀑布流外容器非DOM元素`)
}
// 獲取容器Rect信息
let wrapperBox = opts.wrapper.getBoundingClientRect()
// console.log(wrapperBox.width)
// 可列數
let columnNum = Math.floor(wrapperBox.width / (opts.itemWidth + opts.gutter))
// 元素實際寬度
this.itemWidth = (wrapperBox.width - (columnNum + 1) * opts.gutter) / columnNum
// 列數組
this.columns = Array(columnNum)
this.columns.fill(0, 0)
// console.log(this.columns, this.itemWidth)
}
// 設置數據
setData (arr) {
// 元素添加完成後,重新計算位置
if (Array.isArray(arr) && arr.length) {
let oldLen = this.list.length
this.list = this.list.concat(arr)
this._setPosition(oldLen)
}
}
resetPosition () {
this.init()
this._setPosition(0)
}
_setPosition (start) {
let opts = this.opts
// 間距
let gutter = opts.gutter
// 獲取當前容器內子元素
let childs = opts.wrapper.querySelectorAll(opts.itemSelector)
for (let i = start; i < this.list.length; i++) {
let $item = childs[i]
if (!$item) continue
$item.style.width = this.itemWidth + 'px'
$item.style.display = 'inline-block'
let itemHeight = $item.offsetHeight
// 獲取列高度的最小值
let min = Math.min.apply(null, this.columns)
let index = this.columns.findIndex(i => i === min)
// 設置$item位置
$item.style.top = `${min + gutter}px`
$item.style.left = `${(this.itemWidth + gutter) * index + gutter}px`
// 重置瀑布流當前列高度值
this.columns[index] = min + itemHeight + gutter
// 設置/更新容器最新高度
opts.wrapper.style.minHeight = Math.max.apply(null, this.columns)
}
}
// 清空列表
clearList () {
this.list = []
this.init()
}
/**
* 預加載媒體元素
* @param arr
*/
loadMedia (arr) {
return new Promise(resolve => {
if (Array.isArray(arr) && arr.length) {
let len = arr.length
let count = 0
/* eslint-disable */
function _count () {
count++
if (len === count) resolve()
}
function _loadImage (url) {
let $el = App.createElm('img', {src: url})
$el.onload = _count
$el.onerror = _count
}
arr.forEach(url => {
_loadImage(url)
})
} else {
resolve()
}
})
}
}
export default ZxWaterfall
app.js
export default {
addEvent (el, type, func) {
el.addEventListener(type, func, false)
},
isElement (obj ) {
return !!(obj && obj.nodeType === 1)
},
isObject (obj) {
let type = typeof obj
return type === 'function' || type === 'object' && !!obj
},
createElm (tag, attrs) {
if (this.isObject(tag) && attrs === void 0) {
attrs = tag
tag = void 0
}
const $el = document.createElement(tag || 'div')
for (let key in attrs) {
$el.setAttribute(key, attrs[key])
}
return $el
}
}
使用
demo.vue
<template>
<ul ref="listWrapper" class="list-wrapper">
<li class="item" v-for="(item, index) in list" :key="index" style="display: none">
// item 內容(略)
<img :src="item.imgUrl">
</li>
</ul>
</template>
<script>
import ZxWaterfall from '../waterfall.js'
export default {
data () {
return {
waterfall: null,
list: []
}
},
methods: {
async getList () {
// 從服務器獲取列表數據
let res = await App.post({page: 1})
this.list = this.list.concat(res.list)
// 獲取需要顯示的媒體元素
let medias = res.list.map(item => item.imgUrl)
// 預加載媒體元素
await this.waterfall.loadMedia(medias)
this.waterfall.setData(res.list)
}
},
mounted () {
const $wrapper = this.$refs.listWrapper
// 實例化瀑布流
this.waterfall = new ZxWaterfall({
wrapper: $wrapper,
itemSelector: '.item',
// 元素寬度,大概值
itemWidth: 300,
// 元素間間隔
gutter: 20
})
}
}
</script>