index.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import _mergeJSXProps from 'babel-helper-vue-jsx-merge-props';
  2. import _defineProperty from 'babel-runtime/helpers/defineProperty';
  3. import PropTypes from '../_util/vue-types';
  4. import classNames from 'classnames';
  5. import omit from 'omit.js';
  6. import ResizeObserver from '../vc-resize-observer';
  7. import BaseMixin from '../_util/BaseMixin';
  8. import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
  9. import { ConfigConsumerProps } from '../config-provider/configConsumerProps';
  10. import Base from '../base';
  11. import warning from '../_util/warning';
  12. import { addObserveTarget, removeObserveTarget, getTargetRect, getFixedTop, getFixedBottom } from './utils';
  13. function getDefaultTarget() {
  14. return typeof window !== 'undefined' ? window : null;
  15. }
  16. // Affix
  17. var AffixProps = {
  18. /**
  19. * 距离窗口顶部达到指定偏移量后触发
  20. */
  21. offsetTop: PropTypes.number,
  22. offset: PropTypes.number,
  23. /** 距离窗口底部达到指定偏移量后触发 */
  24. offsetBottom: PropTypes.number,
  25. /** 固定状态改变时触发的回调函数 */
  26. // onChange?: (affixed?: boolean) => void;
  27. /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */
  28. target: PropTypes.func.def(getDefaultTarget),
  29. prefixCls: PropTypes.string
  30. };
  31. var AffixStatus = {
  32. None: 'none',
  33. Prepare: 'Prepare'
  34. };
  35. var Affix = {
  36. name: 'AAffix',
  37. props: AffixProps,
  38. mixins: [BaseMixin],
  39. inject: {
  40. configProvider: { 'default': function _default() {
  41. return ConfigConsumerProps;
  42. } }
  43. },
  44. data: function data() {
  45. return {
  46. affixStyle: undefined,
  47. placeholderStyle: undefined,
  48. status: AffixStatus.None,
  49. lastAffix: false,
  50. prevTarget: null
  51. };
  52. },
  53. beforeMount: function beforeMount() {
  54. this.updatePosition = throttleByAnimationFrame(this.updatePosition);
  55. this.lazyUpdatePosition = throttleByAnimationFrame(this.lazyUpdatePosition);
  56. },
  57. mounted: function mounted() {
  58. var _this = this;
  59. var target = this.target;
  60. if (target) {
  61. // [Legacy] Wait for parent component ref has its value.
  62. // We should use target as directly element instead of function which makes element check hard.
  63. this.timeout = setTimeout(function () {
  64. addObserveTarget(target(), _this);
  65. // Mock Event object.
  66. _this.updatePosition();
  67. });
  68. }
  69. },
  70. updated: function updated() {
  71. this.measure();
  72. },
  73. watch: {
  74. target: function target(val) {
  75. var newTarget = null;
  76. if (val) {
  77. newTarget = val() || null;
  78. }
  79. if (this.prevTarget !== newTarget) {
  80. removeObserveTarget(this);
  81. if (newTarget) {
  82. addObserveTarget(newTarget, this);
  83. // Mock Event object.
  84. this.updatePosition();
  85. }
  86. this.prevTarget = newTarget;
  87. }
  88. },
  89. offsetTop: function offsetTop() {
  90. this.updatePosition();
  91. },
  92. offsetBottom: function offsetBottom() {
  93. this.updatePosition();
  94. }
  95. },
  96. beforeDestroy: function beforeDestroy() {
  97. clearTimeout(this.timeout);
  98. removeObserveTarget(this);
  99. this.updatePosition.cancel();
  100. // https://github.com/ant-design/ant-design/issues/22683
  101. this.lazyUpdatePosition.cancel();
  102. },
  103. methods: {
  104. getOffsetTop: function getOffsetTop() {
  105. var offset = this.offset,
  106. offsetBottom = this.offsetBottom;
  107. var offsetTop = this.offsetTop;
  108. if (typeof offsetTop === 'undefined') {
  109. offsetTop = offset;
  110. warning(typeof offset === 'undefined', 'Affix', '`offset` is deprecated. Please use `offsetTop` instead.');
  111. }
  112. if (offsetBottom === undefined && offsetTop === undefined) {
  113. offsetTop = 0;
  114. }
  115. return offsetTop;
  116. },
  117. getOffsetBottom: function getOffsetBottom() {
  118. return this.offsetBottom;
  119. },
  120. // =================== Measure ===================
  121. measure: function measure() {
  122. var status = this.status,
  123. lastAffix = this.lastAffix;
  124. var target = this.target;
  125. if (status !== AffixStatus.Prepare || !this.$refs.fixedNode || !this.$refs.placeholderNode || !target) {
  126. return;
  127. }
  128. var offsetTop = this.getOffsetTop();
  129. var offsetBottom = this.getOffsetBottom();
  130. var targetNode = target();
  131. if (!targetNode) {
  132. return;
  133. }
  134. var newState = {
  135. status: AffixStatus.None
  136. };
  137. var targetRect = getTargetRect(targetNode);
  138. var placeholderReact = getTargetRect(this.$refs.placeholderNode);
  139. var fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop);
  140. var fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom);
  141. if (fixedTop !== undefined) {
  142. newState.affixStyle = {
  143. position: 'fixed',
  144. top: fixedTop,
  145. width: placeholderReact.width + 'px',
  146. height: placeholderReact.height + 'px'
  147. };
  148. newState.placeholderStyle = {
  149. width: placeholderReact.width + 'px',
  150. height: placeholderReact.height + 'px'
  151. };
  152. } else if (fixedBottom !== undefined) {
  153. newState.affixStyle = {
  154. position: 'fixed',
  155. bottom: fixedBottom,
  156. width: placeholderReact.width + 'px',
  157. height: placeholderReact.height + 'px'
  158. };
  159. newState.placeholderStyle = {
  160. width: placeholderReact.width + 'px',
  161. height: placeholderReact.height + 'px'
  162. };
  163. }
  164. newState.lastAffix = !!newState.affixStyle;
  165. if (lastAffix !== newState.lastAffix) {
  166. this.$emit('change', newState.lastAffix);
  167. }
  168. this.setState(newState);
  169. },
  170. // @ts-ignore TS6133
  171. prepareMeasure: function prepareMeasure() {
  172. this.setState({
  173. status: AffixStatus.Prepare,
  174. affixStyle: undefined,
  175. placeholderStyle: undefined
  176. });
  177. this.$forceUpdate();
  178. // Test if `updatePosition` called
  179. if (process.env.NODE_ENV === 'test') {
  180. this.$emit('testUpdatePosition');
  181. }
  182. },
  183. updatePosition: function updatePosition() {
  184. this.prepareMeasure();
  185. },
  186. lazyUpdatePosition: function lazyUpdatePosition() {
  187. var target = this.target;
  188. var affixStyle = this.affixStyle;
  189. // Check position change before measure to make Safari smooth
  190. if (target && affixStyle) {
  191. var offsetTop = this.getOffsetTop();
  192. var offsetBottom = this.getOffsetBottom();
  193. var targetNode = target();
  194. if (targetNode && this.$refs.placeholderNode) {
  195. var targetRect = getTargetRect(targetNode);
  196. var placeholderReact = getTargetRect(this.$refs.placeholderNode);
  197. var fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop);
  198. var fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom);
  199. if (fixedTop !== undefined && affixStyle.top === fixedTop || fixedBottom !== undefined && affixStyle.bottom === fixedBottom) {
  200. return;
  201. }
  202. }
  203. }
  204. // Directly call prepare measure since it's already throttled.
  205. this.prepareMeasure();
  206. }
  207. },
  208. render: function render() {
  209. var _this2 = this;
  210. var h = arguments[0];
  211. var prefixCls = this.prefixCls,
  212. affixStyle = this.affixStyle,
  213. placeholderStyle = this.placeholderStyle,
  214. $slots = this.$slots,
  215. $props = this.$props;
  216. var getPrefixCls = this.configProvider.getPrefixCls;
  217. var className = classNames(_defineProperty({}, getPrefixCls('affix', prefixCls), affixStyle));
  218. var props = {
  219. attrs: omit($props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target'])
  220. };
  221. return h(
  222. ResizeObserver,
  223. {
  224. on: {
  225. 'resize': function resize() {
  226. _this2.updatePosition();
  227. }
  228. }
  229. },
  230. [h(
  231. 'div',
  232. _mergeJSXProps([props, { style: placeholderStyle, ref: 'placeholderNode' }]),
  233. [h(
  234. 'div',
  235. { 'class': className, ref: 'fixedNode', style: affixStyle },
  236. [$slots['default']]
  237. )]
  238. )]
  239. );
  240. }
  241. };
  242. /* istanbul ignore next */
  243. Affix.install = function (Vue) {
  244. Vue.use(Base);
  245. Vue.component(Affix.name, Affix);
  246. };
  247. export default Affix;