Commit d04fb45a0ebb7342ca3f516a50011e252f1446ff

Authored by Aresn
Committed by GitHub
2 parents a36ef0f2 be01f0b4

Merge pull request #2007 from SergioCrisostomo/scroll-component

New feature: Scroll component
.eslintrc.json
... ... @@ -8,7 +8,8 @@
8 8 "sourceType": "module"
9 9 },
10 10 "env": {
11   - "browser": true
  11 + "browser": true,
  12 + "es6": true
12 13 },
13 14 "extends": "eslint:recommended",
14 15 "plugins": ["vue"],
... ...
package-lock.json
... ... @@ -12511,6 +12511,11 @@
12511 12511 "lodash.escape": "3.2.0"
12512 12512 }
12513 12513 },
  12514 + "lodash.throttle": {
  12515 + "version": "4.1.1",
  12516 + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
  12517 + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
  12518 + },
12514 12519 "lodash.uniq": {
12515 12520 "version": "4.5.0",
12516 12521 "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
... ...
package.json
... ... @@ -45,6 +45,7 @@
45 45 "core-js": "^2.5.0",
46 46 "deepmerge": "^1.5.1",
47 47 "element-resize-detector": "^1.1.12",
  48 + "lodash.throttle": "^4.1.1",
48 49 "popper.js": "^0.6.4",
49 50 "tinycolor2": "^1.4.1"
50 51 },
... ...
src/components/scroll/index.js 0 → 100644
  1 +import Scroll from './scroll.vue';
  2 +
  3 +export default Scroll;
... ...
src/components/scroll/loading-component.vue 0 → 100644
  1 +
  2 +<template lang="html">
  3 + <div :class="wrapperClasses">
  4 + <div :class="spinnerClasses">
  5 + <Spin fix>
  6 + <Icon type="load-c" size="18" :class="iconClasses"></Icon>
  7 + <div v-if="text" :class="textClasses">{{text}}</div>
  8 + </Spin>
  9 +
  10 + </div>
  11 + </div>
  12 +</template>
  13 +
  14 +<script>
  15 +const prefixCls = 'ivu-scroll';
  16 +
  17 +export default {
  18 + props: ['text', 'active', 'spinnerHeight'],
  19 + computed: {
  20 + wrapperClasses() {
  21 + return [
  22 + `${prefixCls}-loader-wrapper`,
  23 + {
  24 + [`${prefixCls}-loader-wrapper-active`]: this.active
  25 + }
  26 + ];
  27 + },
  28 + spinnerClasses() {
  29 + return `${prefixCls}-spinner`;
  30 + },
  31 + iconClasses() {
  32 + return `${prefixCls}-spinner-icon`;
  33 + },
  34 + textClasses() {
  35 + return `${prefixCls}-loader-text`;
  36 + }
  37 + }
  38 +};
  39 +</script>
... ...
src/components/scroll/scroll.vue 0 → 100644
  1 +
  2 +<template>
  3 + <div :class="wrapClasses" style="touch-action: none;">
  4 + <div
  5 + :class="scrollContainerClasses"
  6 + @scroll="handleScroll"
  7 + @wheel="onWheel"
  8 + @touchstart="onPointerDown"
  9 + ref="scrollContainer"
  10 + >
  11 + <div :class="loaderClasses" :style="{paddingTop: wrapperPadding.paddingTop}" ref="toploader">
  12 + <loader :text="loadingText" :active="showTopLoader"></loader>
  13 + </div>
  14 + <div :class="slotContainerClasses" ref="scrollContent">
  15 + <slot></slot>
  16 + </div>
  17 + <div :class="loaderClasses" :style="{paddingBottom: wrapperPadding.paddingBottom}" ref="bottomLoader">
  18 + <loader :text="loadingText" :active="showBottomLoader"></loader>
  19 + </div>
  20 + </div>
  21 + </div>
  22 +</template>
  23 +<script>
  24 + import throttle from 'lodash.throttle';
  25 + import loader from './loading-component.vue';
  26 +
  27 + const prefixCls = 'ivu-scroll';
  28 + const dragConfig = {
  29 + sensitivity: 10,
  30 + minimumStartDragOffset: 5, // minimum start drag offset
  31 + };
  32 +
  33 + export default {
  34 + name: 'Scroll',
  35 + mixins: [],
  36 + components: {loader},
  37 + props: {
  38 + onReachTop: {
  39 + type: Function,
  40 + default: () => Promise.resolve()
  41 + },
  42 + onReachBottom: {
  43 + type: Function,
  44 + default: () => Promise.resolve()
  45 + },
  46 + onReachEdge: {
  47 + type: Function,
  48 + default: () => Promise.resolve()
  49 + },
  50 + loadingText: {
  51 + type: String,
  52 + default: ''
  53 + }
  54 + },
  55 + data() {
  56 + return {
  57 + showTopLoader: false,
  58 + showBottomLoader: false,
  59 + showBodyLoader: false,
  60 + lastScroll: 0,
  61 + reachedTopScrollLimit: true,
  62 + reachedBottomScrollLimit: false,
  63 + topRubberPadding: 0,
  64 + bottomRubberPadding: 0,
  65 + rubberRollBackTimeout: false,
  66 + isLoading: false,
  67 + pointerTouchDown: null,
  68 + touchScroll: false,
  69 + handleScroll: () => {},
  70 + pointerUpHandler: () => {},
  71 + pointerMoveHandler: () => {},
  72 + };
  73 + },
  74 + computed: {
  75 + wrapClasses() {
  76 + return `${prefixCls}-wrapper`;
  77 + },
  78 + scrollContainerClasses() {
  79 + return `${prefixCls}-container`;
  80 + },
  81 + slotContainerClasses() {
  82 + return [
  83 + `${prefixCls}-content`,
  84 + {
  85 + [`${prefixCls}-content-loading`]: this.showBodyLoader
  86 + }
  87 + ];
  88 + },
  89 + loaderClasses() {
  90 + return `${prefixCls}-loader`;
  91 + },
  92 + wrapperPadding() {
  93 + return {
  94 + paddingTop: this.topRubberPadding + 'px',
  95 + paddingBottom: this.bottomRubberPadding + 'px'
  96 + };
  97 + }
  98 + },
  99 + methods: {
  100 + // just to improve feeling of loading and avoid scroll trailing events fired by the browser
  101 + waitOneSecond() {
  102 + return new Promise(resolve => {
  103 + setTimeout(resolve, 2000);
  104 + });
  105 + },
  106 +
  107 + onCallback(dir) {
  108 + this.isLoading = true;
  109 + this.showBodyLoader = true;
  110 + if (dir > 0) {
  111 + this.showTopLoader = true;
  112 + this.topRubberPadding = 20;
  113 + } else {
  114 + this.showBottomLoader = true;
  115 + this.bottomRubberPadding = 20;
  116 +
  117 + // to force the scroll to the bottom while height is animating
  118 + let bottomLoaderHeight = 0;
  119 + const container = this.$refs.scrollContainer;
  120 + const initialScrollTop = container.scrollTop;
  121 + for (var i = 0; i < 20; i++) {
  122 + setTimeout(() => {
  123 + bottomLoaderHeight = Math.max(
  124 + bottomLoaderHeight,
  125 + this.$refs.bottomLoader.getBoundingClientRect().height
  126 + );
  127 + container.scrollTop = initialScrollTop + bottomLoaderHeight;
  128 + }, i * 50);
  129 + }
  130 + }
  131 +
  132 + const callbacks = [this.waitOneSecond(), this.onReachEdge(dir)];
  133 + callbacks.push(dir > 0 ? this.onReachTop() : this.onReachBottom());
  134 +
  135 + let tooSlow = setTimeout(() => {
  136 + this.reset();
  137 + }, 5000);
  138 +
  139 + Promise.all(callbacks).then(() => {
  140 + clearTimeout(tooSlow);
  141 + this.reset();
  142 + });
  143 + },
  144 +
  145 + reset() {
  146 + [
  147 + 'showTopLoader',
  148 + 'showBottomLoader',
  149 + 'showBodyLoader',
  150 + 'isLoading',
  151 + 'reachedTopScrollLimit',
  152 + 'reachedBottomScrollLimit'
  153 + ].forEach(prop => (this[prop] = false));
  154 +
  155 + this.lastScroll = 0;
  156 + this.topRubberPadding = 0;
  157 + this.bottomRubberPadding = 0;
  158 + clearInterval(this.rubberRollBackTimeout);
  159 +
  160 + // if we remove the handler too soon the screen will bump
  161 + if (this.touchScroll) {
  162 + setTimeout(() => {
  163 + window.removeEventListener('touchend', this.pointerUpHandler);
  164 + this.$refs.scrollContainer.removeEventListener('touchmove', this.pointerMoveHandler);
  165 + this.touchScroll = false;
  166 + }, 500);
  167 + }
  168 + },
  169 +
  170 + onWheel() {
  171 + if (this.isLoading) return;
  172 +
  173 + // get the wheel direction
  174 + const wheelDelta = event.wheelDelta ? event.wheelDelta : -(event.detail || event.deltaY);
  175 + this.stretchEdge(wheelDelta);
  176 + },
  177 +
  178 + stretchEdge(direction) {
  179 + clearTimeout(this.rubberRollBackTimeout);
  180 +
  181 + // if the scroll is not strong enough, lets reset it
  182 + this.rubberRollBackTimeout = setTimeout(() => {
  183 + if (!this.isLoading) this.reset();
  184 + }, 250);
  185 +
  186 + // to give the feeling its ruberish and can be puled more to start loading
  187 + if (direction > 0 && this.reachedTopScrollLimit) {
  188 + this.topRubberPadding += 5 - this.topRubberPadding / 5;
  189 + if (this.topRubberPadding > 20) this.onCallback(1);
  190 + } else if (direction < 0 && this.reachedBottomScrollLimit) {
  191 + this.bottomRubberPadding += 6 - this.bottomRubberPadding / 4;
  192 + if (this.bottomRubberPadding > 20) this.onCallback(-1);
  193 + } else {
  194 + this.onScroll();
  195 + }
  196 + },
  197 +
  198 + onScroll() {
  199 + if (this.isLoading) return;
  200 + const el = this.$refs.scrollContainer;
  201 + const scrollDirection = Math.sign(this.lastScroll - el.scrollTop); // IE has no Math.sign, check that webpack polyfills this
  202 + const displacement = el.scrollHeight - el.clientHeight - el.scrollTop;
  203 +
  204 + if (scrollDirection == -1 && displacement <= dragConfig.sensitivity) {
  205 + this.reachedBottomScrollLimit = true;
  206 + } else if (scrollDirection >= 0 && el.scrollTop == 0) {
  207 + this.reachedTopScrollLimit = true;
  208 + } else {
  209 + this.reachedTopScrollLimit = false;
  210 + this.reachedBottomScrollLimit = false;
  211 + this.lastScroll = el.scrollTop;
  212 + }
  213 + },
  214 +
  215 + getTouchCoordinates(e) {
  216 + return {
  217 + x: e.touches[0].pageX,
  218 + y: e.touches[0].pageY
  219 + };
  220 + },
  221 +
  222 + onPointerDown(e) {
  223 + // we just use scroll and wheel in desktop, no mousedown
  224 + if (this.isLoading) return;
  225 + if (e.type == 'touchstart') {
  226 + // if we start do touchmove on the scroll edger the browser will scroll the body
  227 + // by adding 5px margin on pointer down we avoid this behaviour and the scroll/touchmove
  228 + // in the component will not be exported outside of the component
  229 + const container = this.$refs.scrollContainer;
  230 + if (this.reachedTopScrollLimit) container.scrollTop = 5;
  231 + else if (this.reachedBottomScrollLimit) container.scrollTop -= 5;
  232 + }
  233 + if (e.type == 'touchstart' && this.$refs.scrollContainer.scrollTop == 0)
  234 + this.$refs.scrollContainer.scrollTop = 5;
  235 +
  236 + this.pointerTouchDown = this.getTouchCoordinates(e);
  237 + window.addEventListener('touchend', this.pointerUpHandler);
  238 + this.$refs.scrollContainer.parentElement.addEventListener('touchmove', e => {
  239 + e.stopPropagation();
  240 + this.pointerMoveHandler(e);
  241 + }, {passive: false, useCapture: true});
  242 + },
  243 +
  244 + onPointerMove(e) {
  245 + if (!this.pointerTouchDown) return;
  246 + if (this.isLoading) return;
  247 +
  248 + const pointerPosition = this.getTouchCoordinates(e);
  249 + const yDiff = pointerPosition.y - this.pointerTouchDown.y;
  250 +
  251 + this.stretchEdge(yDiff);
  252 +
  253 + if (!this.touchScroll) {
  254 + const wasDragged = Math.abs(yDiff) > dragConfig.minimumStartDragOffset;
  255 + if (wasDragged) this.touchScroll = true;
  256 + else return;
  257 + }
  258 + },
  259 +
  260 + onPointerUp() {
  261 + this.pointerTouchDown = null;
  262 + }
  263 + },
  264 + created(){
  265 + this.handleScroll = throttle(this.onScroll, 150, {leading: false});
  266 + this.pointerUpHandler = this.onPointerUp.bind(this), // because we need the same function to add and remove event handlers
  267 + this.pointerMoveHandler = throttle(this.onPointerMove, 50, {leading: false});
  268 + }
  269 + };
  270 +
  271 +</script>
... ...
src/index.js
... ... @@ -23,6 +23,7 @@ import Form from &#39;./components/form&#39;;
23 23 import Icon from './components/icon';
24 24 import Input from './components/input';
25 25 import InputNumber from './components/input-number';
  26 +import Scroll from './components/scroll';
26 27 import LoadingBar from './components/loading-bar';
27 28 import Menu from './components/menu';
28 29 import Message from './components/message';
... ... @@ -46,8 +47,8 @@ import Tooltip from &#39;./components/tooltip&#39;;
46 47 import Transfer from './components/transfer';
47 48 import Tree from './components/tree';
48 49 import Upload from './components/upload';
49   -import { Row, Col } from './components/grid';
50   -import { Select, Option, OptionGroup } from './components/select';
  50 +import {Row, Col} from './components/grid';
  51 +import {Select, Option, OptionGroup} from './components/select';
51 52 import locale from './locale';
52 53  
53 54 const iview = {
... ... @@ -84,6 +85,7 @@ const iview = {
84 85 Input,
85 86 iInput: Input,
86 87 InputNumber,
  88 + Scroll,
87 89 LoadingBar,
88 90 Menu,
89 91 iMenu: Menu,
... ... @@ -111,7 +113,7 @@ const iview = {
111 113 Spin,
112 114 Step: Steps.Step,
113 115 Steps,
114   - // Switch,
  116 + // Switch,
115 117 iSwitch: Switch,
116 118 iTable: Table,
117 119 Table,
... ... @@ -127,11 +129,11 @@ const iview = {
127 129 Upload
128 130 };
129 131  
130   -const install = function (Vue, opts = {}) {
  132 +const install = function(Vue, opts = {}) {
131 133 locale.use(opts.locale);
132 134 locale.i18n(opts.i18n);
133 135  
134   - Object.keys(iview).forEach((key) => {
  136 + Object.keys(iview).forEach(key => {
135 137 Vue.component(key, iview[key]);
136 138 });
137 139  
... ... @@ -147,4 +149,4 @@ if (typeof window !== &#39;undefined&#39; &amp;&amp; window.Vue) {
147 149 install(window.Vue);
148 150 }
149 151  
150   -module.exports = Object.assign(iview, {install}); // eslint-disable-line no-undef
  152 +module.exports = Object.assign(iview, {install}); // eslint-disable-line no-undef
... ...
src/styles/components/index.less
... ... @@ -13,6 +13,7 @@
13 13 @import "checkbox";
14 14 @import "switch";
15 15 @import "input-number";
  16 +@import "scroll";
16 17 @import "tag";
17 18 @import "loading-bar";
18 19 @import "progress";
... ... @@ -41,4 +42,4 @@
41 42 @import "tree";
42 43 @import "avatar";
43 44 @import "color-picker";
44   -@import "auto-complete";
45 45 \ No newline at end of file
  46 +@import "auto-complete";
... ...
src/styles/components/scroll.less 0 → 100644
  1 +@scroll-prefix-cls: ~"@{css-prefix}scroll";
  2 +
  3 +.@{scroll-prefix-cls} {
  4 + &-wrapper {
  5 + width: auto;
  6 + margin: 0 auto;
  7 + position: relative;
  8 + outline: none;
  9 + }
  10 +
  11 + &-container {
  12 + overflow-y: scroll;
  13 + }
  14 +
  15 + &-content {
  16 + opacity: 1;
  17 + transition: opacity 0.5s;
  18 + }
  19 +
  20 + &-content-loading {
  21 + opacity: 0.5;
  22 + }
  23 +
  24 + &-loader {
  25 + text-align: center;
  26 + padding: 0px;
  27 + transition: padding 0.5s;
  28 + }
  29 +}
  30 +
  31 +.@{scroll-prefix-cls}-loader-wrapper {
  32 + padding: 5px 0;
  33 + height: 0;
  34 + background-color: inherit;
  35 + transform: scale(0);
  36 + transition: opacity .3s, transform .5s, height .5s;
  37 +
  38 + &-active {
  39 + height: 40px;
  40 + transform: scale(1);
  41 + }
  42 +
  43 + @keyframes ani-demo-spin {
  44 + from {
  45 + transform: rotate(0deg);
  46 + }
  47 + 50% {
  48 + transform: rotate(180deg);
  49 + }
  50 + to {
  51 + transform: rotate(360deg);
  52 + }
  53 + }
  54 +
  55 + .@{scroll-prefix-cls}-spinner {
  56 + position: relative;
  57 + }
  58 +
  59 + .@{scroll-prefix-cls}-spinner-icon {
  60 + animation: ani-demo-spin 1s linear infinite;
  61 + }
  62 +}
  63 +
  64 +@media (max-width: 768px) {
  65 + .@{scroll-prefix-cls} {
  66 + }
  67 +}
... ...