diff options
| author | Pitu <[email protected]> | 2018-09-16 01:09:02 -0300 |
|---|---|---|
| committer | Pitu <[email protected]> | 2018-09-16 01:09:02 -0300 |
| commit | fe10a00ba9a3c30d8718ca004ccd19518466f5bd (patch) | |
| tree | 369752f59a88dd03df1e9752be0ba166bf93c933 /src/site/components/grid/waterfall | |
| parent | First version of start script (diff) | |
| download | host.fuwn.me-fe10a00ba9a3c30d8718ca004ccd19518466f5bd.tar.xz host.fuwn.me-fe10a00ba9a3c30d8718ca004ccd19518466f5bd.zip | |
Site
Diffstat (limited to 'src/site/components/grid/waterfall')
| -rw-r--r-- | src/site/components/grid/waterfall/Waterfall.vue | 181 | ||||
| -rw-r--r-- | src/site/components/grid/waterfall/WaterfallItem.vue | 60 | ||||
| -rw-r--r-- | src/site/components/grid/waterfall/old/waterfall-slot.vue | 76 | ||||
| -rw-r--r-- | src/site/components/grid/waterfall/old/waterfall.vue | 442 |
4 files changed, 759 insertions, 0 deletions
diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue new file mode 100644 index 0000000..9827075 --- /dev/null +++ b/src/site/components/grid/waterfall/Waterfall.vue @@ -0,0 +1,181 @@ +<style> + .waterfall { + position: relative; + } +</style> +<template> + <div class="waterfall"> + <slot/> + </div> +</template> +<script> +// import {quickSort, getMinIndex, _, sum} from './util' + +const quickSort = (arr, type) => { + let left = []; + let right = []; + let povis; + if (arr.length <= 1) { + return arr; + } + 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((sum, val) => sum + val); +export default { + name: 'Waterfall', + props: { + gutterWidth: { + type: Number, + default: 0 + }, + gutterHeight: { + type: Number, + default: 0 + }, + resizable: { + type: Boolean, + default: true + }, + align: { + type: String, + default: 'center' + }, + fixWidth: { + type: Number + }, + minCol: { + type: Number, + default: 1 + }, + maxCol: { + type: Number + }, + percent: { + type: Array + } + }, + data() { + return { + timer: null, + colNum: 0, + lastWidth: 0, + percentWidthArr: [] + }; + }, + created() { + this.$on('itemRender', () => { + if (this.timer) { + clearTimeout(this.timer); + } + this.timer = setTimeout(() => { + this.render(); + }, 0); + }); + }, + mounted() { + this.resizeHandle(); + this.$watch('resizable', this.resizeHandle); + }, + methods: { + calulate(arr) { + let 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)); + 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; + } + } + }, + resizeHandle() { + if (this.resizable) { + _.on(window, 'resize', this.render, false); + } else { + _.off(window, 'resize', this.render, false); + } + }, + render() { + // 重新排序 + let childArr = []; + childArr = this.$children.map(child => child.getMeta()); + childArr = quickSort(childArr, 'order'); + // 计算列数 + this.calulate(childArr[0]) + let offsetArr = Array(this.colNum).fill(0); + // 渲染 + childArr.forEach(child => { + let 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.apply(Math, offsetArr)}px`; + }); + this.$emit('rendered', this); + } + } +}; +</script> diff --git a/src/site/components/grid/waterfall/WaterfallItem.vue b/src/site/components/grid/waterfall/WaterfallItem.vue new file mode 100644 index 0000000..597cca6 --- /dev/null +++ b/src/site/components/grid/waterfall/WaterfallItem.vue @@ -0,0 +1,60 @@ +<style> + .waterfall-item { + position: absolute; + } +</style> +<template> + <div class="waterfall-item"> + <slot/> + </div> +</template> +<script> +import imagesLoaded from 'imagesloaded'; +export default { + name: 'WaterfallItem', + props: { + order: { + type: Number, + default: 0 + }, + width: { + type: Number, + default: 150 + } + }, + 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(); + 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> diff --git a/src/site/components/grid/waterfall/old/waterfall-slot.vue b/src/site/components/grid/waterfall/old/waterfall-slot.vue new file mode 100644 index 0000000..07ca268 --- /dev/null +++ b/src/site/components/grid/waterfall/old/waterfall-slot.vue @@ -0,0 +1,76 @@ +<template> + <div class="vue-waterfall-slot" v-show="isShow"> + <slot></slot> + </div> +</template> + +<style> +.vue-waterfall-slot { + position: absolute; + margin: 0; + padding: 0; + box-sizing: border-box; +} +</style> + +<script> + +export default { + data: () => ({ + isShow: false + }), + props: { + width: { + required: true, + validator: (val) => val >= 0 + }, + height: { + required: true, + validator: (val) => val >= 0 + }, + order: { + default: 0 + }, + moveClass: { + default: '' + } + }, + methods: { + notify () { + this.$parent.$emit('reflow', this) + }, + getMeta () { + return { + vm: this, + node: this.$el, + order: this.order, + width: this.width, + height: this.height, + moveClass: this.moveClass + } + } + }, + created () { + this.rect = { + top: 0, + left: 0, + width: 0, + height: 0 + } + this.$watch(() => ( + this.width, + this.height + ), this.notify) + }, + mounted () { + this.$parent.$once('reflowed', () => { + this.isShow = true + }) + this.notify() + }, + destroyed () { + this.notify() + } +} + +</script> diff --git a/src/site/components/grid/waterfall/old/waterfall.vue b/src/site/components/grid/waterfall/old/waterfall.vue new file mode 100644 index 0000000..84e3c98 --- /dev/null +++ b/src/site/components/grid/waterfall/old/waterfall.vue @@ -0,0 +1,442 @@ +<template> + <div class="vue-waterfall" :style="style"> + <slot></slot> + </div> +</template> + +<style> +.vue-waterfall { + position: relative; + /*overflow: hidden; cause clientWidth = 0 in IE if height not bigger than 0 */ +} +</style> + +<script> + +const MOVE_CLASS_PROP = '_wfMoveClass' + +export default { + props: { + autoResize: { + default: true + }, + interval: { + default: 200, + validator: (val) => val >= 0 + }, + align: { + default: 'left', + validator: (val) => ~['left', 'right', 'center'].indexOf(val) + }, + line: { + default: 'v', + validator: (val) => ~['v', 'h'].indexOf(val) + }, + lineGap: { + required: true, + validator: (val) => val >= 0 + }, + minLineGap: { + validator: (val) => val >= 0 + }, + maxLineGap: { + validator: (val) => val >= 0 + }, + singleMaxWidth: { + validator: (val) => val >= 0 + }, + fixedHeight: { + default: false + }, + grow: { + validator: (val) => val instanceof Array + }, + watch: { + default: () => ({}) + } + }, + data: () => ({ + style: { + height: '', + overflow: '' + }, + token: null + }), + methods: { + reflowHandler, + autoResizeHandler, + reflow + }, + created () { + this.virtualRects = [] + this.$on('reflow', () => { + this.reflowHandler() + }) + this.$watch(() => ( + this.align, + this.line, + this.lineGap, + this.minLineGap, + this.maxLineGap, + this.singleMaxWidth, + this.fixedHeight, + this.watch + ), this.reflowHandler) + this.$watch('grow', this.reflowHandler) + }, + mounted () { + this.$watch('autoResize', this.autoResizeHandler) + on(this.$el, getTransitionEndEvent(), tidyUpAnimations, true) + this.autoResizeHandler(this.autoResize) + }, + beforeDestroy () { + this.autoResizeHandler(false) + off(this.$el, getTransitionEndEvent(), tidyUpAnimations, true) + } +} + +function autoResizeHandler (autoResize) { + if (autoResize === false || !this.autoResize) { + off(window, 'resize', this.reflowHandler, false) + } else { + on(window, 'resize', this.reflowHandler, false) + } +} + +function tidyUpAnimations (event) { + let node = event.target + let moveClass = node[MOVE_CLASS_PROP] + if (moveClass) { + removeClass(node, moveClass) + } +} + +function reflowHandler () { + clearTimeout(this.token) + this.token = setTimeout(this.reflow, this.interval) +} + +function reflow () { + if (!this.$el) { return } + let width = this.$el.clientWidth + let metas = this.$children.map((slot) => slot.getMeta()) + metas.sort((a, b) => a.order - b.order) + this.virtualRects = metas.map(() => ({})) + calculate(this, metas, this.virtualRects) + setTimeout(() => { + if (isScrollBarVisibilityChange(this.$el, width)) { + calculate(this, metas, this.virtualRects) + } + this.style.overflow = 'hidden' + render(this.virtualRects, metas) + this.$emit('reflowed', this) + }, 0) +} + +function isScrollBarVisibilityChange (el, lastClientWidth) { + return lastClientWidth !== el.clientWidth +} + +function calculate (vm, metas, styles) { + let options = getOptions(vm) + let processor = vm.line === 'h' ? horizontalLineProcessor : verticalLineProcessor + processor.calculate(vm, options, metas, styles) +} + +function getOptions (vm) { + const maxLineGap = vm.maxLineGap ? +vm.maxLineGap : vm.lineGap + return { + align: ~['left', 'right', 'center'].indexOf(vm.align) ? vm.align : 'left', + line: ~['v', 'h'].indexOf(vm.line) ? vm.line : 'v', + lineGap: +vm.lineGap, + minLineGap: vm.minLineGap ? +vm.minLineGap : vm.lineGap, + maxLineGap: maxLineGap, + singleMaxWidth: Math.max(vm.singleMaxWidth || 0, maxLineGap), + fixedHeight: !!vm.fixedHeight, + grow: vm.grow && vm.grow.map(val => +val) + } +} + +var verticalLineProcessor = (() => { + + function calculate (vm, options, metas, rects) { + let width = vm.$el.clientWidth + let grow = options.grow + let strategy = grow + ? getRowStrategyWithGrow(width, grow) + : getRowStrategy(width, options) + let tops = getArrayFillWith(0, strategy.count) + metas.forEach((meta, index) => { + let offset = tops.reduce((last, top, i) => top < tops[last] ? i : last, 0) + let width = strategy.width[offset % strategy.count] + let rect = rects[index] + rect.top = tops[offset] + rect.left = strategy.left + (offset ? sum(strategy.width.slice(0, offset)) : 0) + rect.width = width + rect.height = meta.height * (options.fixedHeight ? 1 : width / meta.width) + tops[offset] = tops[offset] + rect.height + }) + vm.style.height = Math.max.apply(Math, tops) + 'px' + } + + function getRowStrategy (width, options) { + let count = width / options.lineGap + let slotWidth + if (options.singleMaxWidth >= width) { + count = 1 + slotWidth = Math.max(width, options.minLineGap) + } else { + let maxContentWidth = options.maxLineGap * ~~count + let minGreedyContentWidth = options.minLineGap * ~~(count + 1) + let canFit = maxContentWidth >= width + let canFitGreedy = minGreedyContentWidth <= width + if (canFit && canFitGreedy) { + count = Math.round(count) + slotWidth = width / count + } else if (canFit) { + count = ~~count + slotWidth = width / count + } else if (canFitGreedy) { + count = ~~(count + 1) + slotWidth = width / count + } else { + count = ~~count + slotWidth = options.maxLineGap + } + if (count === 1) { + slotWidth = Math.min(width, options.singleMaxWidth) + slotWidth = Math.max(slotWidth, options.minLineGap) + } + } + return { + width: getArrayFillWith(slotWidth, count), + count: count, + left: getLeft(width, slotWidth * count, options.align) + } + } + + function getRowStrategyWithGrow (width, grow) { + let total = sum(grow) + return { + width: grow.map(val => width * val / total), + count: grow.length, + left: 0 + } + } + + return { + calculate + } + +})() + +var horizontalLineProcessor = (() => { + + function calculate (vm, options, metas, rects) { + let width = vm.$el.clientWidth + let total = metas.length + let top = 0 + let offset = 0 + while (offset < total) { + let strategy = getRowStrategy(width, options, metas, offset) + for (let i = 0, left = 0, meta, rect; i < strategy.count; i++) { + meta = metas[offset + i] + rect = rects[offset + i] + rect.top = top + rect.left = strategy.left + left + rect.width = meta.width * strategy.height / meta.height + rect.height = strategy.height + left += rect.width + } + offset += strategy.count + top += strategy.height + } + vm.style.height = top + 'px' + } + + function getRowStrategy (width, options, metas, offset) { + let greedyCount = getGreedyCount(width, options.lineGap, metas, offset) + let lazyCount = Math.max(greedyCount - 1, 1) + let greedySize = getContentSize(width, options, metas, offset, greedyCount) + let lazySize = getContentSize(width, options, metas, offset, lazyCount) + let finalSize = chooseFinalSize(lazySize, greedySize, width) + let height = finalSize.height + let fitContentWidth = finalSize.width + if (finalSize.count === 1) { + fitContentWidth = Math.min(options.singleMaxWidth, width) + height = metas[offset].height * fitContentWidth / metas[offset].width + } + return { + left: getLeft(width, fitContentWidth, options.align), + count: finalSize.count, + height: height + } + } + + function getGreedyCount (rowWidth, rowHeight, metas, offset) { + let count = 0 + for (let i = offset, width = 0; i < metas.length && width <= rowWidth; i++) { + width += metas[i].width * rowHeight / metas[i].height + count++ + } + return count + } + + function getContentSize (rowWidth, options, metas, offset, count) { + let originWidth = 0 + for (let i = count - 1; i >= 0; i--) { + let meta = metas[offset + i] + originWidth += meta.width * options.lineGap / meta.height + } + let fitHeight = options.lineGap * rowWidth / originWidth + let canFit = (fitHeight <= options.maxLineGap && fitHeight >= options.minLineGap) + if (canFit) { + return { + cost: Math.abs(options.lineGap - fitHeight), + count: count, + width: rowWidth, + height: fitHeight + } + } else { + let height = originWidth > rowWidth ? options.minLineGap : options.maxLineGap + return { + cost: Infinity, + count: count, + width: originWidth * height / options.lineGap, + height: height + } + } + } + + function chooseFinalSize (lazySize, greedySize, rowWidth) { + if (lazySize.cost === Infinity && greedySize.cost === Infinity) { + return greedySize.width < rowWidth ? greedySize : lazySize + } else { + return greedySize.cost >= lazySize.cost ? lazySize : greedySize + } + } + + return { + calculate + } + +})() + +function getLeft (width, contentWidth, align) { + switch (align) { + case 'right': + return width - contentWidth + case 'center': + return (width - contentWidth) / 2 + default: + return 0 + } +} + +function sum (arr) { + return arr.reduce((sum, val) => sum + val) +} + +function render (rects, metas) { + let metasNeedToMoveByTransform = metas.filter((meta) => meta.moveClass) + let firstRects = getRects(metasNeedToMoveByTransform) + applyRects(rects, metas) + let lastRects = getRects(metasNeedToMoveByTransform) + metasNeedToMoveByTransform.forEach((meta, i) => { + meta.node[MOVE_CLASS_PROP] = meta.moveClass + setTransform(meta.node, firstRects[i], lastRects[i]) + }) + document.body.clientWidth // forced reflow + metasNeedToMoveByTransform.forEach((meta) => { + addClass(meta.node, meta.moveClass) + clearTransform(meta.node) + }) +} + +function getRects (metas) { + return metas.map((meta) => meta.vm.rect) +} + +function applyRects (rects, metas) { + rects.forEach((rect, i) => { + let style = metas[i].node.style + metas[i].vm.rect = rect + for (let prop in rect) { + style[prop] = rect[prop] + 'px' + } + }) +} + +function setTransform (node, firstRect, lastRect) { + let dx = firstRect.left - lastRect.left + let dy = firstRect.top - lastRect.top + let sw = firstRect.width / lastRect.width + let sh = firstRect.height / lastRect.height + node.style.transform = + node.style.WebkitTransform = `translate(${dx}px,${dy}px) scale(${sw},${sh})` + node.style.transitionDuration = '0s' +} + +function clearTransform (node) { + node.style.transform = node.style.WebkitTransform = '' + node.style.transitionDuration = '' +} + +function getTransitionEndEvent () { + let isWebkitTrans = + window.ontransitionend === undefined && + window.onwebkittransitionend !== undefined + let transitionEndEvent = isWebkitTrans + ? 'webkitTransitionEnd' + : 'transitionend' + return transitionEndEvent +} + +/** + * util + */ + +function getArrayFillWith (item, count) { + let getter = (typeof item === 'function') ? () => item() : () => item + let arr = [] + for (let i = 0; i < count; i++) { + arr[i] = getter() + } + return arr +} + +function addClass (elem, name) { + if (!hasClass(elem, name)) { + let cur = attr(elem, 'class').trim() + let res = (cur + ' ' + name).trim() + attr(elem, 'class', res) + } +} + +function removeClass (elem, name) { + let reg = new RegExp('\\s*\\b' + name + '\\b\\s*', 'g') + let res = attr(elem, 'class').replace(reg, ' ').trim() + attr(elem, 'class', res) +} + +function hasClass (elem, name) { + return (new RegExp('\\b' + name + '\\b')).test(attr(elem, 'class')) +} + +function attr (elem, name, value) { + if (typeof value !== 'undefined') { + elem.setAttribute(name, value) + } else { + return elem.getAttribute(name) || '' + } +} + +function on (elem, type, listener, useCapture = false) { + elem.addEventListener(type, listener, useCapture) +} + +function off (elem, type, listener, useCapture = false) { + elem.removeEventListener(type, listener, useCapture) +} + +</script> |