273 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Vue
		
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Vue
		
	
	
| 
 | |
| import PropTypes from '../_util/vue-types'
 | |
| import addEventListener from '../_util/Dom/addEventListener'
 | |
| import classNames from 'classnames'
 | |
| import shallowequal from 'shallowequal'
 | |
| import omit from 'omit.js'
 | |
| import getScroll from '../_util/getScroll'
 | |
| import BaseMixin from '../_util/BaseMixin'
 | |
| import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'
 | |
| 
 | |
| function getTargetRect (target) {
 | |
|   return target !== window
 | |
|     ? target.getBoundingClientRect()
 | |
|     : { top: 0, left: 0, bottom: 0 }
 | |
| }
 | |
| 
 | |
| function getOffset (element, target) {
 | |
|   const elemRect = element.getBoundingClientRect()
 | |
|   const targetRect = getTargetRect(target)
 | |
| 
 | |
|   const scrollTop = getScroll(target, true)
 | |
|   const scrollLeft = getScroll(target, false)
 | |
| 
 | |
|   const docElem = window.document.body
 | |
|   const clientTop = docElem.clientTop || 0
 | |
|   const clientLeft = docElem.clientLeft || 0
 | |
| 
 | |
|   return {
 | |
|     top: elemRect.top - targetRect.top +
 | |
|       scrollTop - clientTop,
 | |
|     left: elemRect.left - targetRect.left +
 | |
|       scrollLeft - clientLeft,
 | |
|     width: elemRect.width,
 | |
|     height: elemRect.height,
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getDefaultTarget () {
 | |
|   return typeof window !== 'undefined' ? window : null
 | |
| }
 | |
| 
 | |
| // Affix
 | |
| const AffixProps = {
 | |
|   /**
 | |
|    * 距离窗口顶部达到指定偏移量后触发
 | |
|    */
 | |
|   offsetTop: PropTypes.number,
 | |
|   offset: PropTypes.number,
 | |
|   /** 距离窗口底部达到指定偏移量后触发 */
 | |
|   offsetBottom: PropTypes.number,
 | |
|   /** 固定状态改变时触发的回调函数 */
 | |
|   // onChange?: (affixed?: boolean) => void;
 | |
|   /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */
 | |
|   target: PropTypes.func,
 | |
|   prefixCls: PropTypes.string,
 | |
| }
 | |
| 
 | |
| const Affix = {
 | |
|   name: 'AAffix',
 | |
|   props: AffixProps,
 | |
|   mixins: [BaseMixin],
 | |
|   data () {
 | |
|     this.events = [
 | |
|       'resize',
 | |
|       'scroll',
 | |
|       'touchstart',
 | |
|       'touchmove',
 | |
|       'touchend',
 | |
|       'pageshow',
 | |
|       'load',
 | |
|     ]
 | |
|     this.eventHandlers = {}
 | |
|     return {
 | |
|       affixStyle: undefined,
 | |
|       placeholderStyle: undefined,
 | |
|     }
 | |
|   },
 | |
|   beforeMount () {
 | |
|     this.updatePosition = throttleByAnimationFrame(this.updatePosition)
 | |
|   },
 | |
|   mounted () {
 | |
|     const target = this.target || getDefaultTarget
 | |
|     // Wait for parent component ref has its value
 | |
|     this.timeout = setTimeout(() => {
 | |
|       this.setTargetEventListeners(target)
 | |
|     })
 | |
|   },
 | |
|   watch: {
 | |
|     target (val) {
 | |
|       this.clearEventListeners()
 | |
|       this.setTargetEventListeners(val)
 | |
|       // Mock Event object.
 | |
|       this.updatePosition({})
 | |
|     },
 | |
|     offsetTop (val) {
 | |
|       this.updatePosition({})
 | |
|     },
 | |
|     offsetBottom (val) {
 | |
|       this.updatePosition({})
 | |
|     },
 | |
|   },
 | |
|   beforeDestroy () {
 | |
|     this.clearEventListeners()
 | |
|     clearTimeout(this.timeout)
 | |
|     this.updatePosition.cancel()
 | |
|   },
 | |
|   methods: {
 | |
|     setAffixStyle (e, affixStyle) {
 | |
|       const { target = getDefaultTarget } = this
 | |
|       const originalAffixStyle = this.affixStyle
 | |
|       const isWindow = target() === window
 | |
|       if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) {
 | |
|         return
 | |
|       }
 | |
|       if (shallowequal(affixStyle, originalAffixStyle)) {
 | |
|         return
 | |
|       }
 | |
|       this.setState({ affixStyle: affixStyle }, () => {
 | |
|         const affixed = !!this.affixStyle
 | |
|         if ((affixStyle && !originalAffixStyle) ||
 | |
|           (!affixStyle && originalAffixStyle)) {
 | |
|           this.$emit('change', affixed)
 | |
|         }
 | |
|       })
 | |
|     },
 | |
| 
 | |
|     setPlaceholderStyle (placeholderStyle) {
 | |
|       const originalPlaceholderStyle = this.placeholderStyle
 | |
|       if (shallowequal(placeholderStyle, originalPlaceholderStyle)) {
 | |
|         return
 | |
|       }
 | |
|       this.setState({ placeholderStyle: placeholderStyle })
 | |
|     },
 | |
|     syncPlaceholderStyle (e) {
 | |
|       const { affixStyle } = this
 | |
|       if (!affixStyle) {
 | |
|         return
 | |
|       }
 | |
|       this.$refs.placeholderNode.style.cssText = ''
 | |
|       this.setAffixStyle(e, {
 | |
|         ...affixStyle,
 | |
|         width: this.$refs.placeholderNode.offsetWidth + 'px',
 | |
|       })
 | |
|       this.setPlaceholderStyle({
 | |
|         width: this.$refs.placeholderNode.offsetWidth + 'px',
 | |
|       })
 | |
|     },
 | |
| 
 | |
|     updatePosition (e) {
 | |
|       const { offsetBottom, offset, target = getDefaultTarget } = this
 | |
|       let { offsetTop } = this
 | |
|       const targetNode = target()
 | |
| 
 | |
|       // Backwards support
 | |
|       // Fix: if offsetTop === 0, it will get undefined,
 | |
|       //   if offsetBottom is type of number, offsetMode will be { top: false, ... }
 | |
|       offsetTop = typeof offsetTop === 'undefined' ? offset : offsetTop
 | |
|       const scrollTop = getScroll(targetNode, true)
 | |
|       const affixNode = this.$el
 | |
|       const elemOffset = getOffset(affixNode, targetNode)
 | |
|       const elemSize = {
 | |
|         width: this.$refs.fixedNode.offsetWidth,
 | |
|         height: this.$refs.fixedNode.offsetHeight,
 | |
|       }
 | |
| 
 | |
|       const offsetMode = {
 | |
|         top: false,
 | |
|         bottom: false,
 | |
|       }
 | |
|       // Default to `offsetTop=0`.
 | |
|       if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') {
 | |
|         offsetMode.top = true
 | |
|         offsetTop = 0
 | |
|       } else {
 | |
|         offsetMode.top = typeof offsetTop === 'number'
 | |
|         offsetMode.bottom = typeof offsetBottom === 'number'
 | |
|       }
 | |
| 
 | |
|       const targetRect = getTargetRect(targetNode)
 | |
|       const targetInnerHeight =
 | |
|       targetNode.innerHeight || targetNode.clientHeight
 | |
|       if (scrollTop > elemOffset.top - offsetTop && offsetMode.top) {
 | |
|       // Fixed Top
 | |
|         const width = `${elemOffset.width}px`
 | |
|         const top = `${targetRect.top + offsetTop}px`
 | |
|         this.setAffixStyle(e, {
 | |
|           position: 'fixed',
 | |
|           top,
 | |
|           left: `${targetRect.left + elemOffset.left}px`,
 | |
|           width,
 | |
|         })
 | |
|         this.setPlaceholderStyle({
 | |
|           width,
 | |
|           height: `${elemSize.height}px`,
 | |
|         })
 | |
|       } else if (
 | |
|         scrollTop < elemOffset.top + elemSize.height + offsetBottom - targetInnerHeight &&
 | |
|         offsetMode.bottom
 | |
|       ) {
 | |
|       // Fixed Bottom
 | |
|         const targetBottomOffet = targetNode === window ? 0 : (window.innerHeight - targetRect.bottom)
 | |
|         const width = `${elemOffset.width}px`
 | |
|         this.setAffixStyle(e, {
 | |
|           position: 'fixed',
 | |
|           bottom: targetBottomOffet + offsetBottom + 'px',
 | |
|           left: targetRect.left + elemOffset.left + 'px',
 | |
|           width,
 | |
|         })
 | |
|         this.setPlaceholderStyle({
 | |
|           width,
 | |
|           height: elemOffset.height + 'px',
 | |
|         })
 | |
|       } else {
 | |
|         const { affixStyle } = this
 | |
|         if (e.type === 'resize' && affixStyle && affixStyle.position === 'fixed' && affixNode.offsetWidth) {
 | |
|           this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth + 'px' })
 | |
|         } else {
 | |
|           this.setAffixStyle(e, null)
 | |
|         }
 | |
|         this.setPlaceholderStyle(null)
 | |
|       }
 | |
|       if (e.type === 'resize') {
 | |
|         this.syncPlaceholderStyle(e)
 | |
|       }
 | |
|     },
 | |
|     setTargetEventListeners (getTarget) {
 | |
|       const target = getTarget()
 | |
|       if (!target) {
 | |
|         return
 | |
|       }
 | |
|       this.clearEventListeners()
 | |
| 
 | |
|       this.events.forEach(eventName => {
 | |
|         this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition)
 | |
|       })
 | |
|     },
 | |
| 
 | |
|     clearEventListeners () {
 | |
|       this.events.forEach(eventName => {
 | |
|         const handler = this.eventHandlers[eventName]
 | |
|         if (handler && handler.remove) {
 | |
|           handler.remove()
 | |
|         }
 | |
|       })
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   render () {
 | |
|     const { prefixCls, affixStyle, placeholderStyle, $slots, $props } = this
 | |
|     const className = classNames({
 | |
|       [prefixCls || 'ant-affix']: affixStyle,
 | |
|     })
 | |
| 
 | |
|     const props = {
 | |
|       attrs: omit($props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target']),
 | |
|     }
 | |
|     return (
 | |
|       <div {...props} style={placeholderStyle} ref='placeholderNode'>
 | |
|         <div class={className} ref='fixedNode' style={affixStyle}>
 | |
|           {$slots.default}
 | |
|         </div>
 | |
|       </div>
 | |
|     )
 | |
|   },
 | |
| }
 | |
| 
 | |
| /* istanbul ignore next */
 | |
| Affix.install = function (Vue) {
 | |
|   Vue.component(Affix.name, Affix)
 | |
| }
 | |
| 
 | |
| export default Affix
 |