diff --git a/build/bin/build-entry.js b/build/bin/build-entry.js index d343a9195..fffd532e8 100644 --- a/build/bin/build-entry.js +++ b/build/bin/build-entry.js @@ -53,7 +53,8 @@ ComponentNames.forEach(name => { // services 'Dialog', 'Toast', - 'Indicator' + 'Indicator', + 'Waterfall' ].indexOf(componentName) === -1) { installTemplate.push(render(ISNTALL_COMPONENT_TEMPLATE, { name: componentName, diff --git a/docs/examples/waterfall.md b/docs/examples/waterfall.md index e69de29bb..49420db32 100644 --- a/docs/examples/waterfall.md +++ b/docs/examples/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/index.js b/packages/waterfall/index.js index 59bd195b2..d315e6f96 100644 --- a/packages/waterfall/index.js +++ b/packages/waterfall/index.js @@ -1,3 +1,3 @@ -import SampleComponent from './src/main'; +import Waterfall from './src/main.js'; -export default SampleComponent; +export default Waterfall; 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/main.vue b/packages/waterfall/src/main.vue deleted file mode 100644 index 0d2c38ebd..000000000 --- a/packages/waterfall/src/main.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - 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 5915d777f..abbc3fe65 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,6 @@ const install = function(Vue) { Vue.component(Popup.name, Popup); Vue.component(Picker.name, Picker); Vue.component(RadioGroup.name, RadioGroup); - Vue.component(Waterfall.name, Waterfall); }; // auto install