Commit be01f0b4bb9522b44a34a4bc020ac029f6f864f1
1 parent
d9e0bcc9
New component: Scroll
Showing
9 changed files
with
399 additions
and
9 deletions
Show diff stats
.eslintrc.json
@@ -8,7 +8,8 @@ | @@ -8,7 +8,8 @@ | ||
8 | "sourceType": "module" | 8 | "sourceType": "module" |
9 | }, | 9 | }, |
10 | "env": { | 10 | "env": { |
11 | - "browser": true | 11 | + "browser": true, |
12 | + "es6": true | ||
12 | }, | 13 | }, |
13 | "extends": "eslint:recommended", | 14 | "extends": "eslint:recommended", |
14 | "plugins": ["vue"], | 15 | "plugins": ["vue"], |
package-lock.json
1 | { | 1 | { |
2 | "name": "iview", | 2 | "name": "iview", |
3 | - "version": "2.3.2", | 3 | + "version": "2.4.0", |
4 | "lockfileVersion": 1, | 4 | "lockfileVersion": 1, |
5 | "requires": true, | 5 | "requires": true, |
6 | "dependencies": { | 6 | "dependencies": { |
@@ -12493,6 +12493,11 @@ | @@ -12493,6 +12493,11 @@ | ||
12493 | "lodash.escape": "3.2.0" | 12493 | "lodash.escape": "3.2.0" |
12494 | } | 12494 | } |
12495 | }, | 12495 | }, |
12496 | + "lodash.throttle": { | ||
12497 | + "version": "4.1.1", | ||
12498 | + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", | ||
12499 | + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" | ||
12500 | + }, | ||
12496 | "lodash.uniq": { | 12501 | "lodash.uniq": { |
12497 | "version": "4.5.0", | 12502 | "version": "4.5.0", |
12498 | "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", | 12503 | "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", |
package.json
@@ -44,6 +44,7 @@ | @@ -44,6 +44,7 @@ | ||
44 | "core-js": "^2.5.0", | 44 | "core-js": "^2.5.0", |
45 | "deepmerge": "^1.5.1", | 45 | "deepmerge": "^1.5.1", |
46 | "element-resize-detector": "^1.1.12", | 46 | "element-resize-detector": "^1.1.12", |
47 | + "lodash.throttle": "^4.1.1", | ||
47 | "popper.js": "^0.6.4", | 48 | "popper.js": "^0.6.4", |
48 | "tinycolor2": "^1.4.1" | 49 | "tinycolor2": "^1.4.1" |
49 | }, | 50 | }, |
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> |
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 './components/form'; | @@ -23,6 +23,7 @@ import Form from './components/form'; | ||
23 | import Icon from './components/icon'; | 23 | import Icon from './components/icon'; |
24 | import Input from './components/input'; | 24 | import Input from './components/input'; |
25 | import InputNumber from './components/input-number'; | 25 | import InputNumber from './components/input-number'; |
26 | +import Scroll from './components/scroll'; | ||
26 | import LoadingBar from './components/loading-bar'; | 27 | import LoadingBar from './components/loading-bar'; |
27 | import Menu from './components/menu'; | 28 | import Menu from './components/menu'; |
28 | import Message from './components/message'; | 29 | import Message from './components/message'; |
@@ -46,8 +47,8 @@ import Tooltip from './components/tooltip'; | @@ -46,8 +47,8 @@ import Tooltip from './components/tooltip'; | ||
46 | import Transfer from './components/transfer'; | 47 | import Transfer from './components/transfer'; |
47 | import Tree from './components/tree'; | 48 | import Tree from './components/tree'; |
48 | import Upload from './components/upload'; | 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 | import locale from './locale'; | 52 | import locale from './locale'; |
52 | 53 | ||
53 | const iview = { | 54 | const iview = { |
@@ -84,6 +85,7 @@ const iview = { | @@ -84,6 +85,7 @@ const iview = { | ||
84 | Input, | 85 | Input, |
85 | iInput: Input, | 86 | iInput: Input, |
86 | InputNumber, | 87 | InputNumber, |
88 | + Scroll, | ||
87 | LoadingBar, | 89 | LoadingBar, |
88 | Menu, | 90 | Menu, |
89 | iMenu: Menu, | 91 | iMenu: Menu, |
@@ -111,7 +113,7 @@ const iview = { | @@ -111,7 +113,7 @@ const iview = { | ||
111 | Spin, | 113 | Spin, |
112 | Step: Steps.Step, | 114 | Step: Steps.Step, |
113 | Steps, | 115 | Steps, |
114 | - // Switch, | 116 | + // Switch, |
115 | iSwitch: Switch, | 117 | iSwitch: Switch, |
116 | iTable: Table, | 118 | iTable: Table, |
117 | Table, | 119 | Table, |
@@ -127,11 +129,11 @@ const iview = { | @@ -127,11 +129,11 @@ const iview = { | ||
127 | Upload | 129 | Upload |
128 | }; | 130 | }; |
129 | 131 | ||
130 | -const install = function (Vue, opts = {}) { | 132 | +const install = function(Vue, opts = {}) { |
131 | locale.use(opts.locale); | 133 | locale.use(opts.locale); |
132 | locale.i18n(opts.i18n); | 134 | locale.i18n(opts.i18n); |
133 | 135 | ||
134 | - Object.keys(iview).forEach((key) => { | 136 | + Object.keys(iview).forEach(key => { |
135 | Vue.component(key, iview[key]); | 137 | Vue.component(key, iview[key]); |
136 | }); | 138 | }); |
137 | 139 | ||
@@ -147,4 +149,4 @@ if (typeof window !== 'undefined' && window.Vue) { | @@ -147,4 +149,4 @@ if (typeof window !== 'undefined' && window.Vue) { | ||
147 | install(window.Vue); | 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,6 +13,7 @@ | ||
13 | @import "checkbox"; | 13 | @import "checkbox"; |
14 | @import "switch"; | 14 | @import "switch"; |
15 | @import "input-number"; | 15 | @import "input-number"; |
16 | +@import "scroll"; | ||
16 | @import "tag"; | 17 | @import "tag"; |
17 | @import "loading-bar"; | 18 | @import "loading-bar"; |
18 | @import "progress"; | 19 | @import "progress"; |
@@ -41,4 +42,4 @@ | @@ -41,4 +42,4 @@ | ||
41 | @import "tree"; | 42 | @import "tree"; |
42 | @import "avatar"; | 43 | @import "avatar"; |
43 | @import "color-picker"; | 44 | @import "color-picker"; |
44 | -@import "auto-complete"; | ||
45 | \ No newline at end of file | 45 | \ No newline at end of file |
46 | +@import "auto-complete"; |
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 | +} |