domUtils.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import { isServer } from './is'
  2. const ieVersion = isServer ? 0 : Number((document as any).documentMode)
  3. const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
  4. const MOZ_HACK_REGEXP = /^moz([A-Z])/
  5. export interface ViewportOffsetResult {
  6. left: number
  7. top: number
  8. right: number
  9. bottom: number
  10. rightIncludeBody: number
  11. bottomIncludeBody: number
  12. }
  13. /* istanbul ignore next */
  14. const trim = function (string: string) {
  15. return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
  16. }
  17. /* istanbul ignore next */
  18. const camelCase = function (name: string) {
  19. return name
  20. .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) {
  21. return offset ? letter.toUpperCase() : letter
  22. })
  23. .replace(MOZ_HACK_REGEXP, 'Moz$1')
  24. }
  25. /* istanbul ignore next */
  26. export function hasClass(el: Element, cls: string) {
  27. if (!el || !cls) return false
  28. if (cls.indexOf(' ') !== -1) {
  29. throw new Error('className should not contain space.')
  30. }
  31. if (el.classList) {
  32. return el.classList.contains(cls)
  33. } else {
  34. return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
  35. }
  36. }
  37. /* istanbul ignore next */
  38. export function addClass(el: Element, cls: string) {
  39. if (!el) return
  40. let curClass = el.className
  41. const classes = (cls || '').split(' ')
  42. for (let i = 0, j = classes.length; i < j; i++) {
  43. const clsName = classes[i]
  44. if (!clsName) continue
  45. if (el.classList) {
  46. el.classList.add(clsName)
  47. } else if (!hasClass(el, clsName)) {
  48. curClass += ' ' + clsName
  49. }
  50. }
  51. if (!el.classList) {
  52. el.className = curClass
  53. }
  54. }
  55. /* istanbul ignore next */
  56. export function removeClass(el: Element, cls: string) {
  57. if (!el || !cls) return
  58. const classes = cls.split(' ')
  59. let curClass = ' ' + el.className + ' '
  60. for (let i = 0, j = classes.length; i < j; i++) {
  61. const clsName = classes[i]
  62. if (!clsName) continue
  63. if (el.classList) {
  64. el.classList.remove(clsName)
  65. } else if (hasClass(el, clsName)) {
  66. curClass = curClass.replace(' ' + clsName + ' ', ' ')
  67. }
  68. }
  69. if (!el.classList) {
  70. el.className = trim(curClass)
  71. }
  72. }
  73. export function getBoundingClientRect(element: Element): DOMRect | number {
  74. if (!element || !element.getBoundingClientRect) {
  75. return 0
  76. }
  77. return element.getBoundingClientRect()
  78. }
  79. /**
  80. * 获取当前元素的left、top偏移
  81. * left:元素最左侧距离文档左侧的距离
  82. * top:元素最顶端距离文档顶端的距离
  83. * right:元素最右侧距离文档右侧的距离
  84. * bottom:元素最底端距离文档底端的距离
  85. * rightIncludeBody:元素最左侧距离文档右侧的距离
  86. * bottomIncludeBody:元素最底端距离文档最底部的距离
  87. *
  88. * @description:
  89. */
  90. export function getViewportOffset(element: Element): ViewportOffsetResult {
  91. const doc = document.documentElement
  92. const docScrollLeft = doc.scrollLeft
  93. const docScrollTop = doc.scrollTop
  94. const docClientLeft = doc.clientLeft
  95. const docClientTop = doc.clientTop
  96. const pageXOffset = window.pageXOffset
  97. const pageYOffset = window.pageYOffset
  98. const box = getBoundingClientRect(element)
  99. const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect
  100. const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
  101. const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
  102. const offsetLeft = retLeft + pageXOffset
  103. const offsetTop = rectTop + pageYOffset
  104. const left = offsetLeft - scrollLeft
  105. const top = offsetTop - scrollTop
  106. const clientWidth = window.document.documentElement.clientWidth
  107. const clientHeight = window.document.documentElement.clientHeight
  108. return {
  109. left: left,
  110. top: top,
  111. right: clientWidth - rectWidth - left,
  112. bottom: clientHeight - rectHeight - top,
  113. rightIncludeBody: clientWidth - left,
  114. bottomIncludeBody: clientHeight - top
  115. }
  116. }
  117. /* istanbul ignore next */
  118. export const on = function (
  119. element: HTMLElement | Document | Window,
  120. event: string,
  121. handler: EventListenerOrEventListenerObject
  122. ): void {
  123. if (element && event && handler) {
  124. element.addEventListener(event, handler, false)
  125. }
  126. }
  127. /* istanbul ignore next */
  128. export const off = function (
  129. element: HTMLElement | Document | Window,
  130. event: string,
  131. handler: any
  132. ): void {
  133. if (element && event && handler) {
  134. element.removeEventListener(event, handler, false)
  135. }
  136. }
  137. /* istanbul ignore next */
  138. export const once = function (el: HTMLElement, event: string, fn: EventListener): void {
  139. const listener = function (this: any, ...args: unknown[]) {
  140. if (fn) {
  141. // @ts-ignore
  142. fn.apply(this, args)
  143. }
  144. off(el, event, listener)
  145. }
  146. on(el, event, listener)
  147. }
  148. /* istanbul ignore next */
  149. export const getStyle =
  150. ieVersion < 9
  151. ? function (element: Element | any, styleName: string) {
  152. if (isServer) return
  153. if (!element || !styleName) return null
  154. styleName = camelCase(styleName)
  155. if (styleName === 'float') {
  156. styleName = 'styleFloat'
  157. }
  158. try {
  159. switch (styleName) {
  160. case 'opacity':
  161. try {
  162. return element.filters.item('alpha').opacity / 100
  163. } catch (e) {
  164. return 1.0
  165. }
  166. default:
  167. return element.style[styleName] || element.currentStyle
  168. ? element.currentStyle[styleName]
  169. : null
  170. }
  171. } catch (e) {
  172. return element.style[styleName]
  173. }
  174. }
  175. : function (element: Element | any, styleName: string) {
  176. if (isServer) return
  177. if (!element || !styleName) return null
  178. styleName = camelCase(styleName)
  179. if (styleName === 'float') {
  180. styleName = 'cssFloat'
  181. }
  182. try {
  183. const computed = (document as any).defaultView.getComputedStyle(element, '')
  184. return element.style[styleName] || computed ? computed[styleName] : null
  185. } catch (e) {
  186. return element.style[styleName]
  187. }
  188. }
  189. /* istanbul ignore next */
  190. export function setStyle(element: Element | any, styleName: any, value: any) {
  191. if (!element || !styleName) return
  192. if (typeof styleName === 'object') {
  193. for (const prop in styleName) {
  194. if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
  195. setStyle(element, prop, styleName[prop])
  196. }
  197. }
  198. } else {
  199. styleName = camelCase(styleName)
  200. if (styleName === 'opacity' && ieVersion < 9) {
  201. element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
  202. } else {
  203. element.style[styleName] = value
  204. }
  205. }
  206. }
  207. /* istanbul ignore next */
  208. export const isScroll = (el: Element, vertical: any) => {
  209. if (isServer) return
  210. const determinedDirection = vertical !== null || vertical !== undefined
  211. const overflow = determinedDirection
  212. ? vertical
  213. ? getStyle(el, 'overflow-y')
  214. : getStyle(el, 'overflow-x')
  215. : getStyle(el, 'overflow')
  216. return overflow.match(/(scroll|auto)/)
  217. }
  218. /* istanbul ignore next */
  219. export const getScrollContainer = (el: Element, vertical?: any) => {
  220. if (isServer) return
  221. let parent: any = el
  222. while (parent) {
  223. if ([window, document, document.documentElement].includes(parent)) {
  224. return window
  225. }
  226. if (isScroll(parent, vertical)) {
  227. return parent
  228. }
  229. parent = parent.parentNode
  230. }
  231. return parent
  232. }
  233. /* istanbul ignore next */
  234. export const isInContainer = (el: Element, container: any) => {
  235. if (isServer || !el || !container) return false
  236. const elRect = el.getBoundingClientRect()
  237. let containerRect
  238. if ([window, document, document.documentElement, null, undefined].includes(container)) {
  239. containerRect = {
  240. top: 0,
  241. right: window.innerWidth,
  242. bottom: window.innerHeight,
  243. left: 0
  244. }
  245. } else {
  246. containerRect = container.getBoundingClientRect()
  247. }
  248. return (
  249. elRect.top < containerRect.bottom &&
  250. elRect.bottom > containerRect.top &&
  251. elRect.right > containerRect.left &&
  252. elRect.left < containerRect.right
  253. )
  254. }