Commit be01f0b4bb9522b44a34a4bc020ac029f6f864f1
1 parent
d9e0bcc9
New component: Scroll
Showing
9 changed files
with
399 additions
and
9 deletions
Show diff stats
.eslintrc.json
package-lock.json
1 | 1 | { |
2 | 2 | "name": "iview", |
3 | - "version": "2.3.2", | |
3 | + "version": "2.4.0", | |
4 | 4 | "lockfileVersion": 1, |
5 | 5 | "requires": true, |
6 | 6 | "dependencies": { |
... | ... | @@ -12493,6 +12493,11 @@ |
12493 | 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 | 12501 | "lodash.uniq": { |
12497 | 12502 | "version": "4.5.0", |
12498 | 12503 | "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", | ... | ... |
package.json
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 | 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 './components/tooltip'; |
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 !== 'undefined' && 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"; | ... | ... |
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 | +} | ... | ... |