aboutsummaryrefslogtreecommitdiff
path: root/src/site/components/grid/waterfall
diff options
context:
space:
mode:
authorPitu <[email protected]>2018-09-16 01:09:02 -0300
committerPitu <[email protected]>2018-09-16 01:09:02 -0300
commitfe10a00ba9a3c30d8718ca004ccd19518466f5bd (patch)
tree369752f59a88dd03df1e9752be0ba166bf93c933 /src/site/components/grid/waterfall
parentFirst version of start script (diff)
downloadhost.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.vue181
-rw-r--r--src/site/components/grid/waterfall/WaterfallItem.vue60
-rw-r--r--src/site/components/grid/waterfall/old/waterfall-slot.vue76
-rw-r--r--src/site/components/grid/waterfall/old/waterfall.vue442
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>