index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. /*!
  2. * x-scrollbar 自定义滚动条插件
  3. * 版本: v3.1.0
  4. * 作者: 清晨的阳光(QQ:765550360)
  5. * 许可: MIT
  6. * https://gitee.com/xujz520/x-scrollbar
  7. */
  8. import './index.css'
  9. class XScrollbar {
  10. constructor(dom, options) {
  11. this.$dom = dom
  12. if (this.$dom.classList.contains('x-scrollbar')) return
  13. this.$dom.classList.add('x-scrollbar')
  14. // 移动端检测
  15. this.isMobile = window.navigator.userAgent.toLowerCase().includes('mobile')
  16. // 合并配置
  17. const defaultOptions = {
  18. // 响应容器和内容大小改变(自动更新滚动条)
  19. autoUpdate: true,
  20. // 阻止向上传递滚动事件
  21. preventDefault: true,
  22. // 仅水平滚动(拨动鼠标滚轮时将作用于X轴)
  23. onlyHorizontal: false,
  24. // 自动隐藏
  25. autoHide: true,
  26. }
  27. const defaultStyle = {
  28. // 滑块大小
  29. thumbSize: '5px',
  30. // 轨道颜色
  31. trackBackground: '#ddd',
  32. // 滑块颜色
  33. thumbBackground: '#5f5f5f',
  34. // 滑块圆角大小
  35. thumbRadius: '5px',
  36. }
  37. Object.assign(this, defaultOptions, defaultStyle, options)
  38. // 构造dom
  39. const scrollLeft = this.$dom.scrollLeft
  40. const scrollTop = this.$dom.scrollTop
  41. this.$container = this.html2dom('<div class="x-scrollbar__container"></div>')
  42. this.$content = this.html2dom('<div class="x-scrollbar__content"></div>')
  43. this.$trackX = this.html2dom('<div class="x-scrollbar__track-x"></div>')
  44. this.$trackY = this.html2dom('<div class="x-scrollbar__track-y"></div>')
  45. this.$thumbX = this.html2dom('<div class="x-scrollbar__thumb-x"></div>')
  46. this.$thumbY = this.html2dom('<div class="x-scrollbar__thumb-y"></div>')
  47. this.$trackX.appendChild(this.$thumbX)
  48. this.$trackY.appendChild(this.$thumbY)
  49. const childNodes = []
  50. Array.prototype.forEach.call(this.$dom.childNodes, node => {
  51. childNodes.push(node)
  52. })
  53. childNodes.forEach(node => {
  54. this.$content.appendChild(node)
  55. })
  56. this.$container.appendChild(this.$content)
  57. this.$dom.appendChild(this.$container)
  58. // 处理内边距
  59. const styleObj = getComputedStyle(this.$dom)
  60. const padding = `${styleObj.paddingTop} ${styleObj.paddingRight} ${styleObj.paddingBottom} ${styleObj.paddingLeft}`
  61. if (padding != '0px 0px 0px 0px') {
  62. this.$dom.style.padding = '0px 0px 0px 0px'
  63. this.$container.style.padding = padding
  64. }
  65. // 设置初始值
  66. this.$container.scrollLeft = scrollLeft
  67. this.$container.scrollTop = scrollTop
  68. if (this.preventDefault) {
  69. this.$container.classList.add('x-scrollbar__container--preventDefault')
  70. }
  71. if (this.isMobile) return
  72. this.$dom.appendChild(this.$trackX)
  73. this.$dom.appendChild(this.$trackY)
  74. this.$container.classList.add('x-scrollbar__container--hideScrollbar')
  75. if (JSON.stringify(defaultStyle) != JSON.stringify(Object.keys(defaultStyle).reduce((obj, k) => ({ ...obj, [k]: this[k] }), {}))) {
  76. this.style()
  77. }
  78. // 自动隐藏
  79. if (!this.autoHide) this.$dom.classList.add('x-scrollbar-keep')
  80. // 绑定事件
  81. this.bindScroll()
  82. this.bindDrag()
  83. if (this.onlyHorizontal) {
  84. this.bindWheel()
  85. }
  86. // 响应容器和内容大小改变
  87. if (this.autoUpdate) {
  88. // 首次自动触发
  89. this.resizeObserver()
  90. } else {
  91. this.update()
  92. }
  93. }
  94. /**
  95. * 设置滑块大小
  96. */
  97. setThumbSize() {
  98. // (clientWidth / scrollWidth) = (滑块大小 / clientWidth)
  99. // 最大滑动距离 = clientWidth - 滑块大小
  100. // 最大滚动距离 = scrollWidth - clientWidth
  101. // (滑动距离 / 最大滑动距离) = (滚动距离 / 最大滚动距离)
  102. // 容器大小
  103. this.clientWidth = this.$container.clientWidth
  104. this.clientHeight = this.$container.clientHeight
  105. // 内容大小
  106. this.scrollWidth = this.$container.scrollWidth
  107. this.scrollHeight = this.$container.scrollHeight
  108. //是否存在滚动条
  109. this.hasXScrollbar = this.scrollWidth > this.clientWidth
  110. this.hasYScrollbar = this.scrollHeight > this.clientHeight
  111. //滑块大小
  112. this.thumbXWidth = Math.max((this.clientWidth / this.scrollWidth) * this.clientWidth, 30)
  113. this.thumbYHeight = Math.max((this.clientHeight / this.scrollHeight) * this.clientHeight, 30)
  114. //最大滑动距离
  115. this.thumbXMaxLeft = this.clientWidth - this.thumbXWidth
  116. this.thumbYMaxTop = this.clientHeight - this.thumbYHeight
  117. //最大滚动距离
  118. this.maxScrollLeft = this.scrollWidth - this.clientWidth
  119. this.maxScrollTop = this.scrollHeight - this.clientHeight
  120. this.$trackX.style.display = this.hasXScrollbar ? 'block' : 'none'
  121. this.$trackY.style.display = this.hasYScrollbar ? 'block' : 'none'
  122. this.$thumbX.style.width = `${this.thumbXWidth}px`
  123. this.$thumbY.style.height = `${this.thumbYHeight}px`
  124. }
  125. /**
  126. * 拖动事件
  127. */
  128. bindDrag() {
  129. // 上一次的拖动位置
  130. let screenX = null
  131. let screenY = null
  132. this.$thumbX.addEventListener('mousedown', e => {
  133. this.$trackX.classList.add('x-scrollbar__track--draging')
  134. this.thumbXActive = true
  135. screenX = e.screenX
  136. })
  137. this.$thumbY.addEventListener('mousedown', e => {
  138. this.$trackY.classList.add('x-scrollbar__track--draging')
  139. this.thumbYActive = true
  140. screenY = e.screenY
  141. })
  142. document.addEventListener('mouseup', _ => {
  143. this.$trackX.classList.remove('x-scrollbar__track--draging')
  144. this.$trackY.classList.remove('x-scrollbar__track--draging')
  145. this.thumbXActive = false
  146. this.thumbYActive = false
  147. })
  148. document.addEventListener('mousemove', e => {
  149. if (!(this.thumbXActive || this.thumbYActive)) return
  150. e.preventDefault()
  151. requestAnimationFrame(() => {
  152. if (this.thumbXActive) {
  153. const offset = e.screenX - screenX
  154. screenX = e.screenX
  155. const left = Math.max(Math.min(Number.parseFloat(this.$thumbX.style.left || 0) + offset, this.thumbXMaxLeft), 0)
  156. this.$thumbX.style.left = `${left}px`
  157. this.$container.scrollLeft = (left / this.thumbXMaxLeft) * this.maxScrollLeft
  158. } else {
  159. const offset = e.screenY - screenY
  160. screenY = e.screenY
  161. const top = Math.max(Math.min(Number.parseFloat(this.$thumbY.style.top || 0) + offset, this.thumbYMaxTop), 0)
  162. this.$thumbY.style.top = `${top}px`
  163. this.$container.scrollTop = (top / this.thumbYMaxTop) * this.maxScrollTop
  164. }
  165. })
  166. })
  167. }
  168. /**
  169. * 仅水平滚动(拨动鼠标滚轮时将作用于X轴)
  170. */
  171. bindWheel() {
  172. const easeout = (start, end) => {
  173. if (Math.abs(end - start) <= 1) return end
  174. return start + (end - start) / 4
  175. }
  176. this.$container.addEventListener('wheel', e => {
  177. // 仅响应 y 滚动 => 作用于 x
  178. if (!this.hasXScrollbar) return
  179. if (e.deltaY && !e.shiftKey) {
  180. // 结束值
  181. this.scrollLeft = Math.max(Math.min((this.scrollLeft || this.$container.scrollLeft) + (e.deltaY > 0 ? 100 : -100), this.maxScrollLeft), 0)
  182. this.left = (this.scrollLeft / this.maxScrollLeft) * this.thumbXMaxLeft
  183. // 阻止向上传递 || !(终点)
  184. if (this.preventDefault || !(this.scrollLeft == 0 || this.scrollLeft == this.maxScrollLeft)) {
  185. e.preventDefault()
  186. e.stopPropagation()
  187. }
  188. if (this.reqId) return
  189. // 起始值
  190. let scrollLeft = this.$container.scrollLeft
  191. let left = Number.parseFloat(this.$thumbX.style.left || 0)
  192. const animate = () => {
  193. scrollLeft = easeout(scrollLeft, this.scrollLeft)
  194. left = easeout(left, this.left)
  195. this.$container.scrollLeft = scrollLeft
  196. this.$thumbX.style.left = `${left}px`
  197. this.innerScroll = true
  198. if (scrollLeft != this.scrollLeft) {
  199. this.reqId = requestAnimationFrame(animate)
  200. } else {
  201. this.reqId = null
  202. this.scrollLeft = null
  203. requestAnimationFrame(() => (this.innerScroll = false))
  204. }
  205. }
  206. animate()
  207. }
  208. })
  209. }
  210. /**
  211. * 滚动事件 => 修正滑块位置
  212. */
  213. bindScroll() {
  214. this.$container.addEventListener('scroll', () => {
  215. if (this.thumbXActive || this.thumbYActive || this.innerScroll) return
  216. if (this.hasXScrollbar) {
  217. this.$thumbX.style.left = `${(this.$container.scrollLeft / this.maxScrollLeft) * this.thumbXMaxLeft}px`
  218. }
  219. if (this.hasYScrollbar) {
  220. this.$thumbY.style.top = `${(this.$container.scrollTop / this.maxScrollTop) * this.thumbYMaxTop}px`
  221. }
  222. })
  223. }
  224. /**
  225. * 观察容器大小
  226. */
  227. resizeObserver() {
  228. this.$resizeObserver = new ResizeObserver(entries => {
  229. const contentRect = entries[0].contentRect
  230. if (!(contentRect.width || contentRect.height)) return
  231. this.update()
  232. })
  233. this.$resizeObserver.observe(this.$container)
  234. this.$resizeObserver.observe(this.$content)
  235. }
  236. /**
  237. * 使用滚动值修正滑块
  238. * 在 容器大小 或 内容大小 发生改变时调用
  239. */
  240. update() {
  241. this.setThumbSize()
  242. if (this.hasXScrollbar) {
  243. this.$thumbX.style.left = `${(this.$container.scrollLeft / this.maxScrollLeft) * this.thumbXMaxLeft}px`
  244. }
  245. if (this.hasYScrollbar) {
  246. this.$thumbY.style.top = `${(this.$container.scrollTop / this.maxScrollTop) * this.thumbYMaxTop}px`
  247. }
  248. }
  249. /**
  250. * html字符串 转 dom对象
  251. * @param {*} html
  252. * @returns
  253. */
  254. html2dom(html) {
  255. const element = document.createElement('div')
  256. element.innerHTML = html
  257. const children = element.children
  258. if (children.length <= 1) {
  259. return children[0]
  260. } else {
  261. return children
  262. }
  263. }
  264. /**
  265. * 生成自定义样式
  266. */
  267. style() {
  268. let content = `
  269. /* 轨道 */
  270. .x-scrollbar__track-x {
  271. height: ${Number.parseInt(this.thumbSize) * 2 + 4}px;
  272. }
  273. .x-scrollbar__track-y {
  274. width: ${Number.parseInt(this.thumbSize) * 2 + 4}px;
  275. }
  276. /* 滑块 */
  277. .x-scrollbar__track-x > .x-scrollbar__thumb-x,
  278. .x-scrollbar__track-y > .x-scrollbar__thumb-y {
  279. background: ${this.thumbBackground};
  280. border-radius: ${Number.parseInt(this.thumbRadius || 0) != 5 ? Number.parseInt(this.thumbRadius || 0) : Number.parseInt(this.thumbSize)}px;
  281. }
  282. .x-scrollbar__track-x > .x-scrollbar__thumb-x {
  283. height: ${Number.parseInt(this.thumbSize)}px;
  284. }
  285. .x-scrollbar__track-y > .x-scrollbar__thumb-y {
  286. width: ${Number.parseInt(this.thumbSize)}px;
  287. }
  288. /* 激活后大小 */
  289. .x-scrollbar__track-x:hover > .x-scrollbar__thumb-x,
  290. .x-scrollbar__track--draging > .x-scrollbar__thumb-x {
  291. height: ${Number.parseInt(this.thumbSize) * 2}px;
  292. }
  293. .x-scrollbar__track-y:hover > .x-scrollbar__thumb-y,
  294. .x-scrollbar__track--draging > .x-scrollbar__thumb-y {
  295. width: ${Number.parseInt(this.thumbSize) * 2}px;
  296. }
  297. /* 鼠标移入轨道 || 拖动过程中 => 显示轨道 & 高亮滑块 */
  298. .x-scrollbar__track-x:hover,
  299. .x-scrollbar__track-y:hover,
  300. .x-scrollbar__track-x.x-scrollbar__track--draging,
  301. .x-scrollbar__track-y.x-scrollbar__track--draging {
  302. background: ${this.trackBackground || 'transparent'};
  303. }`
  304. this.key = `x-scrollbar-${Math.abs(Math.trunc((1 + Math.random()) * Date.now())).toString(16)}`
  305. this.$dom.setAttribute(this.key, '')
  306. const style = this.html2dom(`<style ${this.key}></style>`)
  307. content = content.replaceAll('\n.x-scrollbar', `\n[${this.key}] > .x-scrollbar`)
  308. content = content.replaceAll(';', ' !important;')
  309. style.innerHTML = content
  310. document.querySelector('head').appendChild(style)
  311. }
  312. }
  313. export default XScrollbar