@leafer-in/scroller 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/scroller.cjs +259 -0
- package/dist/scroller.esm.js +257 -0
- package/dist/scroller.esm.min.js +2 -0
- package/dist/scroller.esm.min.js.map +1 -0
- package/dist/scroller.js +249 -0
- package/dist/scroller.min.cjs +2 -0
- package/dist/scroller.min.cjs.map +1 -0
- package/dist/scroller.min.js +2 -0
- package/dist/scroller.min.js.map +1 -0
- package/package.json +37 -0
- package/src/Scroller.ts +290 -0
- package/src/config.ts +15 -0
- package/src/decorate.ts +15 -0
- package/src/index.ts +32 -0
- package/types/index.d.ts +47 -0
package/src/Scroller.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { IBounds, IBox, IBoxInputData, IEventListenerId, IOverflow, IScroller, IScrollConfig, IScrollTheme, IObject } from '@leafer-ui/interface'
|
|
2
|
+
import { Group, Box, Bounds, DataHelper, DragEvent, LeafHelper, MoveEvent, DragBoundsHelper, ChildEvent, PointerEvent, Plugin, MathHelper, BranchHelper, isUndefined, BoundsEvent } from '@leafer-ui/core'
|
|
3
|
+
|
|
4
|
+
import { config } from './config'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const tempBounds = new Bounds(), { float } = MathHelper, { clone, assign } = DataHelper
|
|
8
|
+
|
|
9
|
+
export class Scroller extends Group implements IScroller {
|
|
10
|
+
|
|
11
|
+
// 主题 map
|
|
12
|
+
static themeMap: IObject = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
public target: IBox
|
|
16
|
+
|
|
17
|
+
public config: IScrollConfig
|
|
18
|
+
public mergedConfig: IScrollConfig
|
|
19
|
+
|
|
20
|
+
public scrollXBar: IBox
|
|
21
|
+
public scrollYBar: IBox
|
|
22
|
+
|
|
23
|
+
// viewport 区域 / 内容区域
|
|
24
|
+
public ratioX: number
|
|
25
|
+
public ratioY: number
|
|
26
|
+
|
|
27
|
+
// 滚动区域 / 内容区域
|
|
28
|
+
public scrollRatioX: number
|
|
29
|
+
public scrollRatioY: number
|
|
30
|
+
|
|
31
|
+
// scroll之前的内容真实定位
|
|
32
|
+
public contentRealX: number
|
|
33
|
+
public contentRealY: number
|
|
34
|
+
|
|
35
|
+
public dragScrolling: boolean
|
|
36
|
+
|
|
37
|
+
// 用于比对数据,节流
|
|
38
|
+
public targetOverflow: IOverflow
|
|
39
|
+
public targetWorldBounds: IBounds = new Bounds()
|
|
40
|
+
|
|
41
|
+
// viewport 与 内容区域
|
|
42
|
+
public viewportBounds: IBounds = new Bounds()
|
|
43
|
+
public contentBounds: IBounds = new Bounds()
|
|
44
|
+
|
|
45
|
+
// 相对 viewport 区域收缩了一点边距
|
|
46
|
+
public scrollXBounds: IBounds = new Bounds()
|
|
47
|
+
public scrollYBounds: IBounds = new Bounds()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
protected get canUse(): boolean { return this.target.hasScroller }
|
|
51
|
+
|
|
52
|
+
protected hideTimer: any
|
|
53
|
+
|
|
54
|
+
protected __eventIds: IEventListenerId[]
|
|
55
|
+
|
|
56
|
+
constructor(target: IBox) {
|
|
57
|
+
super()
|
|
58
|
+
this.target = target
|
|
59
|
+
this.config = clone(config)
|
|
60
|
+
this.updateConfig()
|
|
61
|
+
this.__listenEvents()
|
|
62
|
+
|
|
63
|
+
target.waitLeafer(() => {
|
|
64
|
+
this.parent = target
|
|
65
|
+
this.__bindLeafer(target.leafer)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (this.mergedConfig.hideOnActionEnd) this.opacity = 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
static registerTheme(theme: IScrollTheme, themeConfig: IScrollConfig): void {
|
|
73
|
+
S.themeMap[theme] = themeConfig
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static getTheme(theme: IScrollTheme): IScrollConfig {
|
|
77
|
+
return theme && S.themeMap[theme]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static hasTheme(theme: IScrollTheme): boolean {
|
|
81
|
+
return theme && !!S.themeMap[theme]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
public updateConfig(): void {
|
|
86
|
+
const { scrollConfig } = this.target
|
|
87
|
+
const themeConfig = S.getTheme((scrollConfig && S.hasTheme(scrollConfig.theme) && scrollConfig.theme) || this.config.theme)
|
|
88
|
+
const mergedConfig: IScrollConfig = this.mergedConfig = clone(this.config)
|
|
89
|
+
assign(mergedConfig, themeConfig)
|
|
90
|
+
if (scrollConfig) assign(mergedConfig, scrollConfig)
|
|
91
|
+
this.updateStyle(mergedConfig.style)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public updateStyle(style: IBoxInputData): void {
|
|
95
|
+
if (!this.scrollXBar) this.addMany(this.scrollXBar = new Box(), this.scrollYBar = new Box())
|
|
96
|
+
const { scrollXBar, scrollYBar } = this
|
|
97
|
+
scrollXBar.set(style)
|
|
98
|
+
scrollYBar.set(style)
|
|
99
|
+
scrollXBar.draggable = 'x'
|
|
100
|
+
scrollYBar.draggable = 'y'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public update(check: boolean = true): void {
|
|
104
|
+
if (this.dragScrolling) return
|
|
105
|
+
|
|
106
|
+
const { target, targetOverflow, targetWorldBounds, viewportBounds, contentBounds } = this, layout = target.__layout, { overflow } = target.__
|
|
107
|
+
|
|
108
|
+
const { childrenRenderBounds } = layout // 内容 bounds
|
|
109
|
+
const { boxBounds, worldBoxBounds } = layout // 容器 bounds
|
|
110
|
+
|
|
111
|
+
const isSameWorldBounds = check && targetOverflow === overflow && targetWorldBounds.isSame(worldBoxBounds)
|
|
112
|
+
const isSameConfig = layout.scrollConfigChanged ? (this.updateConfig(), layout.scrollConfigChanged = false) : true
|
|
113
|
+
|
|
114
|
+
const nowContentBounds = tempBounds.set(viewportBounds).add(childrenRenderBounds)
|
|
115
|
+
|
|
116
|
+
if (isSameWorldBounds && isSameConfig && contentBounds.isSame(nowContentBounds)) return // 节流
|
|
117
|
+
|
|
118
|
+
this.targetOverflow = overflow
|
|
119
|
+
viewportBounds.set(boxBounds)
|
|
120
|
+
targetWorldBounds.set(worldBoxBounds)
|
|
121
|
+
contentBounds.set(nowContentBounds)
|
|
122
|
+
|
|
123
|
+
const { scrollXBar, scrollYBar } = this, { size, endsMargin, minSize } = this.mergedConfig, { width, height } = viewportBounds
|
|
124
|
+
|
|
125
|
+
this.contentRealX = contentBounds.x - target.scrollX
|
|
126
|
+
this.contentRealY = contentBounds.y - target.scrollY
|
|
127
|
+
|
|
128
|
+
this.ratioX = viewportBounds.width / contentBounds.width
|
|
129
|
+
this.ratioY = viewportBounds.height / contentBounds.height
|
|
130
|
+
|
|
131
|
+
const min = size + endsMargin * 2 + minSize
|
|
132
|
+
scrollXBar.visible = float(contentBounds.width) > float(width) && overflow !== 'y-scroll' && width > min
|
|
133
|
+
scrollYBar.visible = float(contentBounds.height) > float(height) && overflow !== 'x-scroll' && height > min
|
|
134
|
+
|
|
135
|
+
this.updateScrollBar()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public updateScrollBar() {
|
|
139
|
+
const { target, viewportBounds, contentBounds, ratioX, ratioY, scrollXBar, scrollYBar, scrollXBounds, scrollYBounds } = this
|
|
140
|
+
let { size, cornerRadius, endsMargin, sideMargin, minSize, scaleFixed, scrollType } = this.mergedConfig
|
|
141
|
+
const scale = scaleFixed ? target.getClampRenderScale() : 1
|
|
142
|
+
|
|
143
|
+
endsMargin /= scale
|
|
144
|
+
sideMargin /= scale
|
|
145
|
+
size /= scale
|
|
146
|
+
if (isUndefined(cornerRadius)) cornerRadius = size / 2
|
|
147
|
+
|
|
148
|
+
if (scrollXBar.visible) {
|
|
149
|
+
scrollXBounds.set(viewportBounds).shrink([endsMargin, scrollYBar.visible ? size + sideMargin : endsMargin, sideMargin, endsMargin])
|
|
150
|
+
const scrollRatioX = this.scrollRatioX = scrollXBounds.width / contentBounds.width
|
|
151
|
+
|
|
152
|
+
scrollXBar.set({
|
|
153
|
+
x: scrollXBounds.x - contentBounds.x * scrollRatioX,
|
|
154
|
+
y: scrollXBounds.maxY - size,
|
|
155
|
+
width: Math.max(scrollXBounds.width * ratioX, minSize),
|
|
156
|
+
height: size,
|
|
157
|
+
cornerRadius,
|
|
158
|
+
dragBounds: scrollXBounds,
|
|
159
|
+
hittable: scrollType !== 'move'
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (scrollYBar.visible) {
|
|
164
|
+
scrollYBounds.set(viewportBounds).shrink([endsMargin, sideMargin, scrollXBar.visible ? size + sideMargin : endsMargin, endsMargin])
|
|
165
|
+
const scrollRatioY = this.scrollRatioY = scrollYBounds.height / contentBounds.height
|
|
166
|
+
|
|
167
|
+
scrollYBar.set({
|
|
168
|
+
x: scrollYBounds.maxX - size,
|
|
169
|
+
y: scrollYBounds.y - contentBounds.y * scrollRatioY,
|
|
170
|
+
width: size,
|
|
171
|
+
height: Math.max(scrollYBounds.height * ratioY, minSize),
|
|
172
|
+
cornerRadius,
|
|
173
|
+
dragBounds: scrollYBounds,
|
|
174
|
+
hittable: scrollType !== 'move'
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.x = -this.target.scrollX
|
|
179
|
+
this.y = -this.target.scrollY
|
|
180
|
+
|
|
181
|
+
LeafHelper.updateAllMatrix(this)
|
|
182
|
+
BranchHelper.updateBounds(this)
|
|
183
|
+
LeafHelper.updateAllChange(this)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected onDrag(e: DragEvent): void {
|
|
187
|
+
if (this.mergedConfig.scrollType === 'move') return
|
|
188
|
+
|
|
189
|
+
this.dragScrolling = true
|
|
190
|
+
|
|
191
|
+
const { scrollXBar, scrollYBar, target, scrollXBounds, scrollYBounds } = this
|
|
192
|
+
const scrollX = e.current === scrollXBar
|
|
193
|
+
|
|
194
|
+
if (scrollX) target.scrollX = -((scrollXBar.x - scrollXBounds.x) / this.scrollRatioX + this.contentRealX)
|
|
195
|
+
else target.scrollY = -((scrollYBar.y - scrollYBounds.y) / this.scrollRatioY + this.contentRealY)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
protected onDragEnd(): void {
|
|
199
|
+
if (this.mergedConfig.scrollType === 'move') return
|
|
200
|
+
|
|
201
|
+
this.dragScrolling = false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
protected onMove(e: MoveEvent): void {
|
|
205
|
+
if (!this.canUse) return
|
|
206
|
+
|
|
207
|
+
this.onEnter()
|
|
208
|
+
|
|
209
|
+
const { scrollType, stopDefault } = this.mergedConfig
|
|
210
|
+
if (scrollType === 'drag') return
|
|
211
|
+
|
|
212
|
+
const { viewportBounds, contentBounds, scrollXBar, scrollYBar } = this
|
|
213
|
+
if (scrollXBar.visible || scrollYBar.visible) {
|
|
214
|
+
const move = e.getInnerMove(this.target)
|
|
215
|
+
DragBoundsHelper.getValidMove(contentBounds, viewportBounds, 'inner', move, true)
|
|
216
|
+
|
|
217
|
+
let needStop: boolean
|
|
218
|
+
if (move.x && scrollXBar.visible) this.target.scrollX += move.x, needStop = true
|
|
219
|
+
if (move.y && scrollYBar.visible) this.target.scrollY += move.y, needStop = true
|
|
220
|
+
if (needStop || stopDefault) e.stop()
|
|
221
|
+
if (stopDefault) e.stopDefault()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
protected onMoveEnd(e: MoveEvent): void {
|
|
226
|
+
if (!this.canUse) return
|
|
227
|
+
|
|
228
|
+
if (!this.target.hit(e)) this.onLeave()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
protected onEnter() {
|
|
232
|
+
if (!this.canUse) return
|
|
233
|
+
|
|
234
|
+
clearTimeout(this.hideTimer)
|
|
235
|
+
|
|
236
|
+
this.killAnimate()
|
|
237
|
+
this.opacity = 1
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
protected onLeave() {
|
|
241
|
+
if (!this.canUse) return
|
|
242
|
+
|
|
243
|
+
clearTimeout(this.hideTimer)
|
|
244
|
+
|
|
245
|
+
if (this.mergedConfig.hideOnActionEnd) this.hideTimer = setTimeout(() => {
|
|
246
|
+
this.set({ opacity: 0 }, Plugin.has('animate'))
|
|
247
|
+
}, 600)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
protected onResize() {
|
|
251
|
+
if (this.canUse) this.update()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected __listenEvents(): void {
|
|
255
|
+
const { scrollXBar, scrollYBar, target } = this
|
|
256
|
+
this.__eventIds = [
|
|
257
|
+
scrollXBar.on_(DragEvent.DRAG, this.onDrag, this),
|
|
258
|
+
scrollXBar.on_(DragEvent.END, this.onDragEnd, this),
|
|
259
|
+
|
|
260
|
+
scrollYBar.on_(DragEvent.DRAG, this.onDrag, this),
|
|
261
|
+
scrollYBar.on_(DragEvent.END, this.onDragEnd, this),
|
|
262
|
+
|
|
263
|
+
target.on_(PointerEvent.ENTER, this.onEnter, this),
|
|
264
|
+
target.on_(PointerEvent.LEAVE, this.onLeave, this),
|
|
265
|
+
|
|
266
|
+
target.on_(MoveEvent.BEFORE_MOVE, this.onMove, this),
|
|
267
|
+
target.on_(MoveEvent.END, this.onMoveEnd, this),
|
|
268
|
+
|
|
269
|
+
target.on_(BoundsEvent.WORLD, this.onResize, this), // 更新
|
|
270
|
+
target.on_(ChildEvent.DESTROY, this.destroy, this)
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
protected __removeListenEvents(): void {
|
|
275
|
+
this.off_(this.__eventIds)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public destroy(): void {
|
|
279
|
+
if (!this.destroyed) {
|
|
280
|
+
this.__removeListenEvents()
|
|
281
|
+
const { target } = this
|
|
282
|
+
target.scroller = target.topChildren = target.hasScroller = undefined
|
|
283
|
+
this.target = this.config = null
|
|
284
|
+
super.destroy()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const S = Scroller
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IScrollConfig } from '@leafer-ui/interface'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const config: IScrollConfig = {
|
|
5
|
+
theme: 'light',
|
|
6
|
+
style: { dragBoundsType: 'outer', strokeAlign: 'center', strokeWidthFixed: 'zoom-in', width: 6, height: 6, opacity: 0.5, cornerRadius: 3, hoverStyle: { opacity: 0.6 }, pressStyle: { opacity: 0.66 } },
|
|
7
|
+
size: 6,
|
|
8
|
+
endsMargin: 2,
|
|
9
|
+
sideMargin: 2,
|
|
10
|
+
minSize: 10,
|
|
11
|
+
scaleFixed: 'zoom-in',
|
|
12
|
+
scrollType: 'both',
|
|
13
|
+
hideOnActionEnd: 'hover'
|
|
14
|
+
}
|
|
15
|
+
|
package/src/decorate.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IValue } from '@leafer-ui/interface'
|
|
2
|
+
import { decorateLeafAttr, attr, doBoundsType } from '@leafer-ui/core'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export function scrollConfigType(defaultValue?: IValue) {
|
|
6
|
+
return decorateLeafAttr(defaultValue, (key: string) => attr({
|
|
7
|
+
set(value: IValue) {
|
|
8
|
+
if (this.__setAttr(key, value)) {
|
|
9
|
+
const layout = this.__layout
|
|
10
|
+
layout.scrollConfigChanged = true
|
|
11
|
+
doBoundsType(this)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}))
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { Scroller } from './Scroller'
|
|
2
|
+
|
|
3
|
+
import { Plugin, Box } from '@leafer-ui/core'
|
|
4
|
+
import { Scroller } from './Scroller'
|
|
5
|
+
import { scrollConfigType } from './decorate'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Plugin.add('scroller')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const box = Box.prototype
|
|
12
|
+
|
|
13
|
+
Box.addAttr('scrollConfig', undefined, scrollConfigType)
|
|
14
|
+
|
|
15
|
+
box.__checkScroll = function (isScrollMode: boolean) {
|
|
16
|
+
if (isScrollMode && this.isOverflow) {
|
|
17
|
+
if (!this.scroller) {
|
|
18
|
+
this.scroller = new Scroller(this)
|
|
19
|
+
if (!this.topChildren) this.topChildren = []
|
|
20
|
+
this.topChildren.push(this.scroller)
|
|
21
|
+
}
|
|
22
|
+
this.hasScroller = true
|
|
23
|
+
} else {
|
|
24
|
+
if (this.hasScroller && !this.scroller.dragScrolling) {
|
|
25
|
+
this.hasScroller = undefined
|
|
26
|
+
this.scroller.update()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Scroller.registerTheme('light', { style: { fill: 'black' } }) // 白天模式
|
|
32
|
+
Scroller.registerTheme('dark', { style: { fill: 'white' } }) // 夜间模式
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IScroller, IObject, IBox, IScrollConfig, IOverflow, IBounds, IEventListenerId, IScrollTheme, IBoxInputData } from '@leafer-ui/interface';
|
|
2
|
+
import { Group, DragEvent, MoveEvent } from '@leafer-ui/core';
|
|
3
|
+
|
|
4
|
+
declare class Scroller extends Group implements IScroller {
|
|
5
|
+
static themeMap: IObject;
|
|
6
|
+
target: IBox;
|
|
7
|
+
config: IScrollConfig;
|
|
8
|
+
mergedConfig: IScrollConfig;
|
|
9
|
+
scrollXBar: IBox;
|
|
10
|
+
scrollYBar: IBox;
|
|
11
|
+
ratioX: number;
|
|
12
|
+
ratioY: number;
|
|
13
|
+
scrollRatioX: number;
|
|
14
|
+
scrollRatioY: number;
|
|
15
|
+
contentRealX: number;
|
|
16
|
+
contentRealY: number;
|
|
17
|
+
dragScrolling: boolean;
|
|
18
|
+
targetOverflow: IOverflow;
|
|
19
|
+
targetWorldBounds: IBounds;
|
|
20
|
+
viewportBounds: IBounds;
|
|
21
|
+
contentBounds: IBounds;
|
|
22
|
+
scrollXBounds: IBounds;
|
|
23
|
+
scrollYBounds: IBounds;
|
|
24
|
+
protected get canUse(): boolean;
|
|
25
|
+
protected hideTimer: any;
|
|
26
|
+
protected __eventIds: IEventListenerId[];
|
|
27
|
+
constructor(target: IBox);
|
|
28
|
+
static registerTheme(theme: IScrollTheme, themeConfig: IScrollConfig): void;
|
|
29
|
+
static getTheme(theme: IScrollTheme): IScrollConfig;
|
|
30
|
+
static hasTheme(theme: IScrollTheme): boolean;
|
|
31
|
+
updateConfig(): void;
|
|
32
|
+
updateStyle(style: IBoxInputData): void;
|
|
33
|
+
update(check?: boolean): void;
|
|
34
|
+
updateScrollBar(): void;
|
|
35
|
+
protected onDrag(e: DragEvent): void;
|
|
36
|
+
protected onDragEnd(): void;
|
|
37
|
+
protected onMove(e: MoveEvent): void;
|
|
38
|
+
protected onMoveEnd(e: MoveEvent): void;
|
|
39
|
+
protected onEnter(): void;
|
|
40
|
+
protected onLeave(): void;
|
|
41
|
+
protected onResize(): void;
|
|
42
|
+
protected __listenEvents(): void;
|
|
43
|
+
protected __removeListenEvents(): void;
|
|
44
|
+
destroy(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { Scroller };
|