diff --git a/build/bin/build-entry.js b/build/bin/build-entry.js
index d43620767..a3a9a1a35 100644
--- a/build/bin/build-entry.js
+++ b/build/bin/build-entry.js
@@ -51,7 +51,8 @@ ComponentNames.forEach(name => {
// services
'Dialog',
'Toast',
- 'Indicator'
+ 'Indicator',
+ 'Waterfall'
].indexOf(componentName) === -1) {
installTemplate.push(render(ISNTALL_COMPONENT_TEMPLATE, {
name: componentName,
diff --git a/components.json b/components.json
index 5d694f816..3b1dc2789 100644
--- a/components.json
+++ b/components.json
@@ -10,6 +10,7 @@
"dialog": "./packages/dialog/index.js",
"picker": "./packages/picker/index.js",
"radio-group": "./packages/radio-group/index.js",
+ "waterfall": "./packages/waterfall/index.js",
"loading": "./packages/loading/index.js",
"panel": "./packages/panel/index.js",
"card": "./packages/card/index.js"
diff --git a/docs/examples-docs/waterfall.md b/docs/examples-docs/waterfall.md
new file mode 100644
index 000000000..49420db32
--- /dev/null
+++ b/docs/examples-docs/waterfall.md
@@ -0,0 +1,84 @@
+
+
+
+## Waterfall组件
+
+### 基础用法
+
+:::demo 样例代码
+```html
+
+
+
+ {{ item }}
+
+
+ loading
+
+
+
+```
+:::
+
+### API
+
+| 参数 | 说明 | 类型 | 默认值 | 可选值 |
+|-----------|-----------|-----------|-------------|-------------|
+| waterfall-disabled | 是否禁止瀑布流触发 | Boolean | false | |
+| waterfall-offset | 触发瀑布流加载的阈值 | Number | 300 | |
+
diff --git a/packages/waterfall/CHANGELOG.md b/packages/waterfall/CHANGELOG.md
new file mode 100644
index 000000000..e88c472b3
--- /dev/null
+++ b/packages/waterfall/CHANGELOG.md
@@ -0,0 +1,8 @@
+## 0.0.2 (2017-01-20)
+
+* 改了bug A
+* 加了功能B
+
+## 0.0.1 (2017-01-10)
+
+* 第一版
diff --git a/packages/waterfall/README.md b/packages/waterfall/README.md
new file mode 100644
index 000000000..13efadb2c
--- /dev/null
+++ b/packages/waterfall/README.md
@@ -0,0 +1,26 @@
+# @youzan/waterfall
+
+!!! 请在此处填写你的文档最简单描述 !!!
+
+[![version][version-image]][download-url]
+[![download][download-image]][download-url]
+
+[version-image]: http://npm.qima-inc.com/badge/v/@youzan/<%= name %>.svg?style=flat-square
+[download-image]: http://npm.qima-inc.com/badge/d/@youzan/<%= name %>.svg?style=flat-square
+[download-url]: http://npm.qima-inc.com/package/@youzan/<%= name %>
+
+## Demo
+
+## Usage
+
+## API
+
+| 参数 | 说明 | 类型 | 默认值 | 可选值 |
+|-----------|-----------|-----------|-------------|-------------|
+| className | 自定义额外类名 | string | '' | '' |
+
+
+
+
+## License
+[MIT](https://opensource.org/licenses/MIT)
diff --git a/packages/waterfall/index.js b/packages/waterfall/index.js
new file mode 100644
index 000000000..d315e6f96
--- /dev/null
+++ b/packages/waterfall/index.js
@@ -0,0 +1,3 @@
+import Waterfall from './src/main.js';
+
+export default Waterfall;
diff --git a/packages/waterfall/package.json b/packages/waterfall/package.json
new file mode 100644
index 000000000..ec57bc0d4
--- /dev/null
+++ b/packages/waterfall/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "waterfall",
+ "version": "0.0.1",
+ "description": "瀑布流",
+ "main": "./lib/index.js",
+ "author": "pangxie1991",
+ "license": "MIT",
+ "devDependencies": {},
+ "dependencies": {}
+}
diff --git a/packages/waterfall/src/directive.js b/packages/waterfall/src/directive.js
new file mode 100644
index 000000000..48f342e63
--- /dev/null
+++ b/packages/waterfall/src/directive.js
@@ -0,0 +1,94 @@
+import Utils from './utils.js';
+
+const CONTEXT = '@@Waterfall';
+const OFFSET = 300;
+
+// 绑定事件到元素上
+// 读取基本的控制变量
+function doBindEvent() {
+ this.scrollEventListener = Utils.debounce(handleScrollEvent.bind(this), 200);
+ this.scrollEventTarget = Utils.getScrollEventTarget(this.el);
+
+ var disabledExpr = this.el.getAttribute('waterfall-disabled');
+ var disabled = false;
+ if (disabledExpr) {
+ this.vm.$watch(disabledExpr, (value) => {
+ this.disabled = value;
+ });
+ disabled = Boolean(this.vm[disabledExpr]);
+ }
+ this.disabled = disabled;
+
+ var offset = this.el.getAttribute('waterfall-offset');
+ this.offset = Number(offset) || OFFSET;
+
+ this.scrollEventTarget.addEventListener('scroll', this.scrollEventListener);
+
+ this.scrollEventListener();
+}
+
+// 处理滚动函数
+function handleScrollEvent() {
+ let element = this.el;
+ let scrollEventTarget = this.scrollEventTarget;
+
+ // 已被禁止的滚动处理
+ if (this.disabled) return;
+
+ let targetScrollTop = Utils.getScrollTop(scrollEventTarget);
+ let targetBottom = targetScrollTop + Utils.getVisibleHeight(scrollEventTarget);
+
+ // 判断是否到了底
+ let needLoadMoreToLower = false;
+ if (element === scrollEventTarget) {
+ needLoadMoreToLower = scrollEventTarget.scollHeight - targetBottom < this.offset;
+ } else {
+ let elementBottom = Utils.getElementTop(element) - Utils.getElementTop(scrollEventTarget) + Utils.getVisibleHeight(element);
+ needLoadMoreToLower = elementBottom - Utils.getVisibleHeight(scrollEventTarget) < this.offset;
+ }
+ if (needLoadMoreToLower) {
+ this.cb['lower'] && this.cb['lower']({ target: scrollEventTarget, top: targetScrollTop });
+ }
+
+ // 判断是否到了顶
+ let needLoadMoreToUpper = false;
+ if (element === scrollEventTarget) {
+ needLoadMoreToUpper = targetScrollTop < this.offset;
+ } else {
+ let elementTop = Utils.getElementTop(element) - Utils.getElementTop(scrollEventTarget);
+ needLoadMoreToUpper = elementTop + this.offset > 0;
+ }
+ if (needLoadMoreToUpper) {
+ this.cb['upper'] && this.cb['upper']({ target: scrollEventTarget, top: targetScrollTop });
+ }
+}
+
+export default function(type) {
+ return {
+ bind(el, binding, vnode) {
+ if (!el[CONTEXT]) {
+ el[CONTEXT] = {
+ el,
+ vm: vnode.context,
+ cb: {}
+ };
+ }
+ el[CONTEXT].cb[type] = binding.value;
+
+ vnode.context.$on('hook:mounted', function() {
+ if (Utils.isAttached(el)) {
+ doBindEvent.call(el[CONTEXT]);
+ }
+ });
+ },
+
+ update(el) {
+ el[CONTEXT].scrollEventListener();
+ },
+
+ unbind(el) {
+ const context = el[CONTEXT];
+ context.scrollEventTarget.removeEventListener('scroll', context.scrollEventListener);
+ }
+ };
+};
diff --git a/packages/waterfall/src/main.js b/packages/waterfall/src/main.js
new file mode 100644
index 000000000..02665b9a1
--- /dev/null
+++ b/packages/waterfall/src/main.js
@@ -0,0 +1,14 @@
+import Waterfall from './directive.js';
+import Vue from 'vue';
+
+const install = function(Vue) {
+ Vue.directive('WaterfallLower', Waterfall('lower'));
+ Vue.directive('WaterfallUpper', Waterfall('upper'));
+};
+
+if (!Vue.prototype.$isServer) {
+ Vue.use(install);
+}
+
+Waterfall.install = install;
+export default Waterfall;
diff --git a/packages/waterfall/src/utils.js b/packages/waterfall/src/utils.js
new file mode 100644
index 000000000..5f113ad53
--- /dev/null
+++ b/packages/waterfall/src/utils.js
@@ -0,0 +1,75 @@
+export default {
+ debounce(func, wait, immediate) {
+ var timeout, args, context, timestamp, result;
+ return function() {
+ context = this;
+ args = arguments;
+ timestamp = new Date();
+ var later = function() {
+ var last = (new Date()) - timestamp;
+ if (last < wait) {
+ timeout = setTimeout(later, wait - last);
+ } else {
+ timeout = null;
+ result = func.apply(context, args);
+ }
+ };
+ if (!timeout) {
+ timeout = setTimeout(later, wait);
+ }
+ return result;
+ };
+ },
+
+ // 找到最近的触发滚动事件的元素
+ getScrollEventTarget(element) {
+ var currentNode = element;
+ // bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome
+ while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
+ var overflowY = this.getComputedStyle(currentNode).overflowY;
+ if (overflowY === 'scroll' || overflowY === 'auto') {
+ return currentNode;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ return window;
+ },
+
+ // 判断元素是否被加入到页面节点内
+ isAttached(element) {
+ var currentNode = element.parentNode;
+ while (currentNode) {
+ if (currentNode.tagName === 'HTML') {
+ return true;
+ }
+ if (currentNode.nodeType === 11) {
+ return false;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ return false;
+ },
+
+ // 获取滚动高度
+ getScrollTop(element) {
+ return 'scrollTop' in element ? element.scrollTop : element.pageYOffset;
+ },
+
+ // 获取元素距离顶部高度
+ getElementTop(element) {
+ if (element === window) {
+ return this.getScrollTop(window);
+ }
+ return element.getBoundingClientRect().top + this.getScrollTop(window);
+ },
+
+ getVisibleHeight(element) {
+ if (element === window) {
+ return element.innerHeight;
+ }
+
+ return element.getBoundingClientRect().height;
+ },
+
+ getComputedStyle: document.defaultView.getComputedStyle.bind(document.defaultView)
+};
diff --git a/src/index.js b/src/index.js
index 9dd314a94..12bab1ca2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -9,6 +9,7 @@ import Popup from '../packages/popup/index.js';
import Dialog from '../packages/dialog/index.js';
import Picker from '../packages/picker/index.js';
import RadioGroup from '../packages/radio-group/index.js';
+import Waterfall from '../packages/waterfall/index.js';
import Loading from '../packages/loading/index.js';
import Panel from '../packages/panel/index.js';
import Card from '../packages/card/index.js';
@@ -50,6 +51,7 @@ module.exports = {
Dialog,
Picker,
RadioGroup,
+ Waterfall,
Loading,
Panel,
Card