1 | import { BScrollInstance, propertiesConfig } from './Instance'
|
2 | import { Options, DefOptions, OptionsConstructor } from './Options'
|
3 | import Scroller from './scroller/Scroller'
|
4 | import {
|
5 | getElement,
|
6 | warn,
|
7 | isUndef,
|
8 | propertiesProxy,
|
9 | ApplyOrder,
|
10 | EventEmitter
|
11 | } from '@better-scroll/shared-utils'
|
12 | import { bubbling } from './utils/bubbling'
|
13 | import { UnionToIntersection } from './utils/typesHelper'
|
14 |
|
15 | interface PluginCtor {
|
16 | pluginName: string
|
17 | applyOrder?: ApplyOrder
|
18 | new (scroll: BScroll): any
|
19 | }
|
20 |
|
21 | interface PluginItem {
|
22 | name: string
|
23 | applyOrder?: ApplyOrder.Pre | ApplyOrder.Post
|
24 | ctor: PluginCtor
|
25 | }
|
26 | interface PluginsMap {
|
27 | [key: string]: boolean
|
28 | }
|
29 | interface PropertyConfig {
|
30 | key: string
|
31 | sourceKey: string
|
32 | }
|
33 |
|
34 | type ElementParam = HTMLElement | string
|
35 |
|
36 | export interface MountedBScrollHTMLElement extends HTMLElement {
|
37 | isBScrollContainer?: boolean
|
38 | }
|
39 |
|
40 | export class BScrollConstructor<O = {}> extends EventEmitter {
|
41 | static plugins: PluginItem[] = []
|
42 | static pluginsMap: PluginsMap = {}
|
43 | scroller: Scroller
|
44 | options: OptionsConstructor
|
45 | hooks: EventEmitter
|
46 | plugins: { [name: string]: any }
|
47 | wrapper: HTMLElement
|
48 | content: HTMLElement;
|
49 | [key: string]: any
|
50 |
|
51 | static use(ctor: PluginCtor) {
|
52 | const name = ctor.pluginName
|
53 | const installed = BScrollConstructor.plugins.some(
|
54 | plugin => ctor === plugin.ctor
|
55 | )
|
56 | if (installed) return BScrollConstructor
|
57 | if (isUndef(name)) {
|
58 | warn(
|
59 | `Plugin Class must specify plugin's name in static property by 'pluginName' field.`
|
60 | )
|
61 | return BScrollConstructor
|
62 | }
|
63 | BScrollConstructor.pluginsMap[name] = true
|
64 | BScrollConstructor.plugins.push({
|
65 | name,
|
66 | applyOrder: ctor.applyOrder,
|
67 | ctor
|
68 | })
|
69 | return BScrollConstructor
|
70 | }
|
71 |
|
72 | constructor(el: ElementParam, options?: Options & O) {
|
73 | super([
|
74 | 'refresh',
|
75 | 'contentChanged',
|
76 | 'enable',
|
77 | 'disable',
|
78 | 'beforeScrollStart',
|
79 | 'scrollStart',
|
80 | 'scroll',
|
81 | 'scrollEnd',
|
82 | 'scrollCancel',
|
83 | 'touchEnd',
|
84 | 'flick',
|
85 | 'destroy'
|
86 | ])
|
87 |
|
88 | const wrapper = getElement(el)
|
89 |
|
90 | if (!wrapper) {
|
91 | warn('Can not resolve the wrapper DOM.')
|
92 | return
|
93 | }
|
94 |
|
95 | this.plugins = {}
|
96 | this.options = new OptionsConstructor().merge(options).process()
|
97 |
|
98 | if (!this.setContent(wrapper).valid) {
|
99 | return
|
100 | }
|
101 |
|
102 | this.hooks = new EventEmitter([
|
103 | 'refresh',
|
104 | 'enable',
|
105 | 'disable',
|
106 | 'destroy',
|
107 | 'beforeInitialScrollTo',
|
108 | 'contentChanged'
|
109 | ])
|
110 | this.init(wrapper)
|
111 | }
|
112 |
|
113 | setContent(wrapper: MountedBScrollHTMLElement) {
|
114 | let contentChanged = false
|
115 | let valid = true
|
116 | const content = wrapper.children[
|
117 | this.options.specifiedIndexAsContent
|
118 | ] as HTMLElement
|
119 | if (!content) {
|
120 | warn(
|
121 | 'The wrapper need at least one child element to be content element to scroll.'
|
122 | )
|
123 | valid = false
|
124 | } else {
|
125 | contentChanged = this.content !== content
|
126 | if (contentChanged) {
|
127 | this.content = content
|
128 | }
|
129 | }
|
130 | return {
|
131 | valid,
|
132 | contentChanged
|
133 | }
|
134 | }
|
135 |
|
136 | private init(wrapper: MountedBScrollHTMLElement) {
|
137 | this.wrapper = wrapper
|
138 |
|
139 |
|
140 | wrapper.isBScrollContainer = true
|
141 | this.scroller = new Scroller(wrapper, this.content, this.options)
|
142 | this.scroller.hooks.on(this.scroller.hooks.eventTypes.resize, () => {
|
143 | this.refresh()
|
144 | })
|
145 |
|
146 | this.eventBubbling()
|
147 | this.handleAutoBlur()
|
148 | this.enable()
|
149 |
|
150 | this.proxy(propertiesConfig)
|
151 | this.applyPlugins()
|
152 |
|
153 |
|
154 | this.refreshWithoutReset(this.content)
|
155 | const { startX, startY } = this.options
|
156 | const position = {
|
157 | x: startX,
|
158 | y: startY
|
159 | }
|
160 |
|
161 | if (
|
162 | this.hooks.trigger(this.hooks.eventTypes.beforeInitialScrollTo, position)
|
163 | ) {
|
164 | return
|
165 | }
|
166 | this.scroller.scrollTo(position.x, position.y)
|
167 | }
|
168 |
|
169 | private applyPlugins() {
|
170 | const options = this.options
|
171 | BScrollConstructor.plugins
|
172 | .sort((a, b) => {
|
173 | const applyOrderMap = {
|
174 | [ApplyOrder.Pre]: -1,
|
175 | [ApplyOrder.Post]: 1
|
176 | }
|
177 | const aOrder = a.applyOrder ? applyOrderMap[a.applyOrder] : 0
|
178 | const bOrder = b.applyOrder ? applyOrderMap[b.applyOrder] : 0
|
179 | return aOrder - bOrder
|
180 | })
|
181 | .forEach((item: PluginItem) => {
|
182 | const ctor = item.ctor
|
183 | if (options[item.name] && typeof ctor === 'function') {
|
184 | this.plugins[item.name] = new ctor(this)
|
185 | }
|
186 | })
|
187 | }
|
188 |
|
189 | private handleAutoBlur() {
|
190 |
|
191 | if (this.options.autoBlur) {
|
192 | this.on(this.eventTypes.beforeScrollStart, () => {
|
193 | let activeElement = document.activeElement as HTMLElement
|
194 | if (
|
195 | activeElement &&
|
196 | (activeElement.tagName === 'INPUT' ||
|
197 | activeElement.tagName === 'TEXTAREA')
|
198 | ) {
|
199 | activeElement.blur()
|
200 | }
|
201 | })
|
202 | }
|
203 | }
|
204 |
|
205 | private eventBubbling() {
|
206 | bubbling(this.scroller.hooks, this, [
|
207 | this.eventTypes.beforeScrollStart,
|
208 | this.eventTypes.scrollStart,
|
209 | this.eventTypes.scroll,
|
210 | this.eventTypes.scrollEnd,
|
211 | this.eventTypes.scrollCancel,
|
212 | this.eventTypes.touchEnd,
|
213 | this.eventTypes.flick
|
214 | ])
|
215 | }
|
216 |
|
217 | private refreshWithoutReset(content: HTMLElement) {
|
218 | this.scroller.refresh(content)
|
219 | this.hooks.trigger(this.hooks.eventTypes.refresh, content)
|
220 | this.trigger(this.eventTypes.refresh, content)
|
221 | }
|
222 |
|
223 | proxy(propertiesConfig: PropertyConfig[]) {
|
224 | propertiesConfig.forEach(({ key, sourceKey }) => {
|
225 | propertiesProxy(this, sourceKey, key)
|
226 | })
|
227 | }
|
228 | refresh() {
|
229 | const { contentChanged, valid } = this.setContent(this.wrapper)
|
230 | if (valid) {
|
231 | const content = this.content
|
232 | this.refreshWithoutReset(content)
|
233 | if (contentChanged) {
|
234 | this.hooks.trigger(this.hooks.eventTypes.contentChanged, content)
|
235 | this.trigger(this.eventTypes.contentChanged, content)
|
236 | }
|
237 | this.scroller.resetPosition()
|
238 | }
|
239 | }
|
240 |
|
241 | enable() {
|
242 | this.scroller.enable()
|
243 | this.hooks.trigger(this.hooks.eventTypes.enable)
|
244 | this.trigger(this.eventTypes.enable)
|
245 | }
|
246 |
|
247 | disable() {
|
248 | this.scroller.disable()
|
249 | this.hooks.trigger(this.hooks.eventTypes.disable)
|
250 | this.trigger(this.eventTypes.disable)
|
251 | }
|
252 |
|
253 | destroy() {
|
254 | this.hooks.trigger(this.hooks.eventTypes.destroy)
|
255 | this.trigger(this.eventTypes.destroy)
|
256 | this.scroller.destroy()
|
257 | }
|
258 | eventRegister(names: string[]) {
|
259 | this.registerType(names)
|
260 | }
|
261 | }
|
262 |
|
263 | export interface BScrollConstructor extends BScrollInstance {}
|
264 |
|
265 | export interface CustomAPI {
|
266 | [key: string]: {}
|
267 | }
|
268 |
|
269 | type ExtractAPI<O> = {
|
270 | [K in keyof O]: K extends string
|
271 | ? DefOptions[K] extends undefined
|
272 | ? CustomAPI[K]
|
273 | : never
|
274 | : never
|
275 | }[keyof O]
|
276 |
|
277 | export function createBScroll<O = {}>(
|
278 | el: ElementParam,
|
279 | options?: Options & O
|
280 | ): BScrollConstructor & UnionToIntersection<ExtractAPI<O>> {
|
281 | const bs = new BScrollConstructor(el, options)
|
282 | return (bs as unknown) as BScrollConstructor &
|
283 | UnionToIntersection<ExtractAPI<O>>
|
284 | }
|
285 |
|
286 | createBScroll.use = BScrollConstructor.use
|
287 | createBScroll.plugins = BScrollConstructor.plugins
|
288 | createBScroll.pluginsMap = BScrollConstructor.pluginsMap
|
289 |
|
290 | type createBScroll = typeof createBScroll
|
291 | export interface BScrollFactory extends createBScroll {
|
292 | new <O = {}>(el: ElementParam, options?: Options & O): BScrollConstructor &
|
293 | UnionToIntersection<ExtractAPI<O>>
|
294 | }
|
295 |
|
296 | export type BScroll<O = Options> = BScrollConstructor<O> &
|
297 | UnionToIntersection<ExtractAPI<O>>
|
298 |
|
299 | export const BScroll = (createBScroll as unknown) as BScrollFactory
|