UNPKG

7.67 kBPlain TextView Raw
1import { BScrollInstance, propertiesConfig } from './Instance'
2import { Options, DefOptions, OptionsConstructor } from './Options'
3import Scroller from './scroller/Scroller'
4import {
5 getElement,
6 warn,
7 isUndef,
8 propertiesProxy,
9 ApplyOrder,
10 EventEmitter
11} from '@better-scroll/shared-utils'
12import { bubbling } from './utils/bubbling'
13import { UnionToIntersection } from './utils/typesHelper'
14
15interface PluginCtor {
16 pluginName: string
17 applyOrder?: ApplyOrder
18 new (scroll: BScroll): any
19}
20
21interface PluginItem {
22 name: string
23 applyOrder?: ApplyOrder.Pre | ApplyOrder.Post
24 ctor: PluginCtor
25}
26interface PluginsMap {
27 [key: string]: boolean
28}
29interface PropertyConfig {
30 key: string
31 sourceKey: string
32}
33
34type ElementParam = HTMLElement | string
35
36export interface MountedBScrollHTMLElement extends HTMLElement {
37 isBScrollContainer?: boolean
38}
39
40export 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 // mark wrapper to recognize bs instance by DOM attribute
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 // maybe boundary has changed, should refresh
154 this.refreshWithoutReset(this.content)
155 const { startX, startY } = this.options
156 const position = {
157 x: startX,
158 y: startY
159 }
160 // maybe plugins want to control scroll position
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 /* istanbul ignore if */
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
263export interface BScrollConstructor extends BScrollInstance {}
264
265export interface CustomAPI {
266 [key: string]: {}
267}
268
269type 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
277export 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
286createBScroll.use = BScrollConstructor.use
287createBScroll.plugins = BScrollConstructor.plugins
288createBScroll.pluginsMap = BScrollConstructor.pluginsMap
289
290type createBScroll = typeof createBScroll
291export interface BScrollFactory extends createBScroll {
292 new <O = {}>(el: ElementParam, options?: Options & O): BScrollConstructor &
293 UnionToIntersection<ExtractAPI<O>>
294}
295
296export type BScroll<O = Options> = BScrollConstructor<O> &
297 UnionToIntersection<ExtractAPI<O>>
298
299export const BScroll = (createBScroll as unknown) as BScrollFactory