DomManager.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { pListItem } from './DataManager'
  2. import Tombstone from './Tombstone'
  3. import { style, cssVendor } from '@better-scroll/shared-utils'
  4. const ANIMATION_DURATION_MS = 200
  5. export default class DomManager {
  6. private content: HTMLElement
  7. private unusedDom: HTMLElement[] = []
  8. private timers: Array<number> = []
  9. constructor(
  10. content: HTMLElement,
  11. private renderFn: (data: any, div?: HTMLElement) => HTMLElement,
  12. private tombstone: Tombstone
  13. ) {
  14. this.setContent(content)
  15. }
  16. update(
  17. list: Array<pListItem>,
  18. start: number,
  19. end: number
  20. ): {
  21. start: number
  22. end: number
  23. startPos: number
  24. startDelta: number
  25. endPos: number
  26. } {
  27. if (start >= list.length) {
  28. start = list.length - 1
  29. }
  30. if (end > list.length) {
  31. end = list.length
  32. }
  33. this.collectUnusedDom(list, start, end)
  34. this.createDom(list, start, end)
  35. this.cacheHeight(list, start, end)
  36. const { startPos, startDelta, endPos } = this.positionDom(list, start, end)
  37. return {
  38. start,
  39. startPos,
  40. startDelta,
  41. end,
  42. endPos,
  43. }
  44. }
  45. private collectUnusedDom(
  46. list: Array<pListItem>,
  47. start: number,
  48. end: number
  49. ): Array<any> {
  50. // TODO optimise
  51. for (let i = 0; i < list.length; i++) {
  52. if (i === start) {
  53. i = end - 1
  54. continue
  55. }
  56. if (list[i].dom) {
  57. const dom = list[i].dom as HTMLElement
  58. if (Tombstone.isTombstone(dom)) {
  59. this.tombstone.recycleOne(dom)
  60. dom.style.display = 'none'
  61. } else {
  62. this.unusedDom.push(dom)
  63. }
  64. list[i].dom = null
  65. }
  66. }
  67. return list
  68. }
  69. private createDom(list: Array<pListItem>, start: number, end: number): void {
  70. for (let i = start; i < end; i++) {
  71. let dom = list[i].dom
  72. const data = list[i].data
  73. if (dom) {
  74. if (Tombstone.isTombstone(dom) && data) {
  75. list[i].tombstone = dom
  76. list[i].dom = null
  77. } else {
  78. continue
  79. }
  80. }
  81. dom = data
  82. ? this.renderFn(data, this.unusedDom.pop())
  83. : this.tombstone.getOne()
  84. dom.style.position = 'absolute'
  85. list[i].dom = dom
  86. list[i].pos = -1
  87. this.content.appendChild(dom)
  88. }
  89. }
  90. private cacheHeight(
  91. list: Array<pListItem>,
  92. start: number,
  93. end: number
  94. ): void {
  95. for (let i = start; i < end; i++) {
  96. if (list[i].data && !list[i].height) {
  97. list[i].height = list[i].dom!.offsetHeight
  98. }
  99. }
  100. }
  101. private positionDom(
  102. list: Array<pListItem>,
  103. start: number,
  104. end: number
  105. ): { startPos: number; startDelta: number; endPos: number } {
  106. const tombstoneEles: Array<HTMLElement> = []
  107. const { start: startPos, delta: startDelta } = this.getStartPos(
  108. list,
  109. start,
  110. end
  111. )
  112. let pos = startPos
  113. for (let i = start; i < end; i++) {
  114. const tombstone = list[i].tombstone
  115. if (tombstone) {
  116. const tombstoneStyle = tombstone.style as any
  117. tombstoneStyle[
  118. style.transition
  119. ] = `${cssVendor}transform ${ANIMATION_DURATION_MS}ms, opacity ${ANIMATION_DURATION_MS}ms`
  120. tombstoneStyle[style.transform] = `translateY(${pos}px)`
  121. tombstoneStyle.opacity = '0'
  122. list[i].tombstone = null
  123. tombstoneEles.push(tombstone)
  124. }
  125. if (list[i].dom && list[i].pos !== pos) {
  126. list[i].dom!.style[style.transform as any] = `translateY(${pos}px)`
  127. list[i].pos = pos
  128. }
  129. pos += list[i].height || this.tombstone.height
  130. }
  131. const timerId = window.setTimeout(() => {
  132. this.tombstone.recycle(tombstoneEles)
  133. }, ANIMATION_DURATION_MS)
  134. this.timers.push(timerId)
  135. return {
  136. startPos,
  137. startDelta,
  138. endPos: pos,
  139. }
  140. }
  141. private getStartPos(
  142. list: Array<any>,
  143. start: number,
  144. end: number
  145. ): { start: number; delta: number } {
  146. if (list[start] && list[start].pos !== -1) {
  147. return {
  148. start: list[start].pos,
  149. delta: 0,
  150. }
  151. }
  152. // TODO optimise
  153. let pos = list[0].pos === -1 ? 0 : list[0].pos
  154. for (let i = 0; i < start; i++) {
  155. pos += list[i].height || this.tombstone.height
  156. }
  157. let originPos = pos
  158. let i
  159. for (i = start; i < end; i++) {
  160. if (!Tombstone.isTombstone(list[i].dom) && list[i].pos !== -1) {
  161. pos = list[i].pos
  162. break
  163. }
  164. }
  165. let x = i
  166. if (x < end) {
  167. while (x > start) {
  168. pos -= list[x - 1].height
  169. x--
  170. }
  171. }
  172. const delta = originPos - pos
  173. return {
  174. start: pos,
  175. delta: delta,
  176. }
  177. }
  178. removeTombstone(): void {
  179. const tombstones = this.content.querySelectorAll('.tombstone')
  180. for (let i = tombstones.length - 1; i >= 0; i--) {
  181. this.content.removeChild(tombstones[i])
  182. }
  183. }
  184. setContent(content: HTMLElement) {
  185. if (content !== this.content) {
  186. this.content = content
  187. }
  188. }
  189. destroy(): void {
  190. this.removeTombstone()
  191. this.timers.forEach((id) => {
  192. clearTimeout(id)
  193. })
  194. }
  195. resetState() {
  196. this.destroy()
  197. this.timers = []
  198. this.unusedDom = []
  199. }
  200. }