@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.
@@ -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
+
@@ -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' } }) // 夜间模式
@@ -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 };