foundation.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. /* eslint-disable no-param-reassign */
  2. /* eslint-disable prefer-destructuring, max-lines-per-function, one-var, max-len, @typescript-eslint/restrict-plus-operands */
  3. /* argus-disable unPkgSensitiveInfo */
  4. import { get, isEmpty } from 'lodash';
  5. import { DOMRectLikeType } from '../utils/dom';
  6. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  7. import { ArrayElement } from '../utils/type';
  8. import { strings } from './constants';
  9. const REGS = {
  10. TOP: /top/i,
  11. RIGHT: /right/i,
  12. BOTTOM: /bottom/i,
  13. LEFT: /left/i,
  14. };
  15. const defaultRect = {
  16. left: 0,
  17. top: 0,
  18. height: 0,
  19. width: 0,
  20. scrollLeft: 0,
  21. scrollTop: 0,
  22. };
  23. export interface TooltipAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  24. registerPortalEvent(portalEventSet: any): void;
  25. unregisterPortalEvent(): void;
  26. registerResizeHandler(onResize: () => void): void;
  27. unregisterResizeHandler(onResize?: () => void): void;
  28. on(arg0: string, arg1: () => void): void;
  29. notifyVisibleChange(isVisible: any): void;
  30. getPopupContainerRect(): PopupContainerDOMRect;
  31. containerIsBody(): boolean;
  32. off(arg0: string): void;
  33. canMotion(): boolean;
  34. registerScrollHandler(arg: () => Record<string, any>): void;
  35. unregisterScrollHandler(): void;
  36. insertPortal(...args: any[]): void;
  37. removePortal(...args: any[]): void;
  38. getEventName(): {
  39. mouseEnter: string;
  40. mouseLeave: string;
  41. mouseOut: string;
  42. mouseOver: string;
  43. click: string;
  44. focus: string;
  45. blur: string;
  46. };
  47. registerTriggerEvent(...args: any[]): void;
  48. getTriggerBounding(...args: any[]): DOMRect;
  49. getWrapperBounding(...args: any[]): DOMRect;
  50. setPosition(...args: any[]): void;
  51. togglePortalVisible(...args: any[]): void;
  52. registerClickOutsideHandler(...args: any[]): void;
  53. unregisterClickOutsideHandler(...args: any[]): void;
  54. unregisterTriggerEvent(): void;
  55. containerIsRelative(): boolean;
  56. containerIsRelativeOrAbsolute(): boolean;
  57. getDocumentElementBounding(): DOMRect;
  58. updateContainerPosition(): void;
  59. updatePlacementAttr(placement: Position): void;
  60. getContainerPosition(): string;
  61. }
  62. export type Position = ArrayElement<typeof strings.POSITION_SET>;
  63. export interface PopupContainerDOMRect extends DOMRectLikeType {
  64. scrollLeft?: number;
  65. scrollTop?: number;
  66. }
  67. export default class Tooltip<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TooltipAdapter<P, S>, P, S> {
  68. _timer: ReturnType<typeof setTimeout>;
  69. _mounted: boolean;
  70. constructor(adapter: TooltipAdapter<P, S>) {
  71. super({ ...adapter });
  72. this._timer = null;
  73. }
  74. init() {
  75. this._mounted = true;
  76. this._bindEvent();
  77. this._shouldShow();
  78. this._initContainerPosition();
  79. }
  80. destroy() {
  81. this._mounted = false;
  82. this._unBindEvent();
  83. }
  84. _bindEvent() {
  85. const trigger = this.getProp('trigger'); // get trigger type
  86. const { triggerEventSet, portalEventSet } = this._generateEvent(trigger);
  87. this._bindTriggerEvent(triggerEventSet);
  88. this._bindPortalEvent(portalEventSet);
  89. this._bindResizeEvent();
  90. }
  91. _unBindEvent() {
  92. this._unBindTriggerEvent();
  93. this._unBindPortalEvent();
  94. this._unBindResizeEvent();
  95. this._unBindScrollEvent();
  96. }
  97. _bindTriggerEvent(triggerEventSet: Record<string, any>) {
  98. this._adapter.registerTriggerEvent(triggerEventSet);
  99. }
  100. _unBindTriggerEvent() {
  101. this._adapter.unregisterTriggerEvent();
  102. }
  103. _bindPortalEvent(portalEventSet: Record<string, any>) {
  104. this._adapter.registerPortalEvent(portalEventSet);
  105. }
  106. _unBindPortalEvent() {
  107. this._adapter.unregisterPortalEvent();
  108. }
  109. _bindResizeEvent() {
  110. this._adapter.registerResizeHandler(this.onResize);
  111. }
  112. _unBindResizeEvent() {
  113. this._adapter.unregisterResizeHandler(this.onResize);
  114. }
  115. _reversePos(position = '', isVertical = false) {
  116. if (isVertical) {
  117. if (REGS.TOP.test(position)) {
  118. return position.replace('top', 'bottom').replace('Top', 'Bottom');
  119. } else if (REGS.BOTTOM.test(position)) {
  120. return position.replace('bottom', 'top').replace('Bottom', 'Top');
  121. }
  122. } else if (REGS.LEFT.test(position)) {
  123. return position.replace('left', 'right').replace('Left', 'Right');
  124. } else if (REGS.RIGHT.test(position)) {
  125. return position.replace('right', 'left').replace('Right', 'Left');
  126. }
  127. return position;
  128. }
  129. clearDelayTimer() {
  130. if (this._timer) {
  131. clearTimeout(this._timer);
  132. this._timer = null;
  133. }
  134. }
  135. _generateEvent(types: ArrayElement<typeof strings.TRIGGER_SET>) {
  136. const eventNames = this._adapter.getEventName();
  137. const triggerEventSet = {};
  138. let portalEventSet = {};
  139. switch (types) {
  140. case 'focus':
  141. triggerEventSet[eventNames.focus] = () => {
  142. this.delayShow();
  143. };
  144. triggerEventSet[eventNames.blur] = () => {
  145. this.delayHide();
  146. };
  147. portalEventSet = triggerEventSet;
  148. break;
  149. case 'click':
  150. triggerEventSet[eventNames.click] = () => {
  151. // this.delayShow();
  152. this.show();
  153. };
  154. portalEventSet = {};
  155. // Click outside needs special treatment, can not be directly tied to the trigger Element, need to be bound to the document
  156. break;
  157. case 'hover':
  158. triggerEventSet[eventNames.mouseEnter] = () => {
  159. // console.log(e);
  160. this.setCache('isClickToHide', false);
  161. this.delayShow();
  162. // this.show('trigger');
  163. };
  164. triggerEventSet[eventNames.mouseLeave] = () => {
  165. // console.log(e);
  166. this.delayHide();
  167. // this.hide('trigger');
  168. };
  169. portalEventSet = { ...triggerEventSet };
  170. if (this.getProp('clickToHide')) {
  171. portalEventSet[eventNames.click] = () => {
  172. this.setCache('isClickToHide', true);
  173. this.hide();
  174. };
  175. portalEventSet[eventNames.mouseEnter] = () => {
  176. if (this.getCache('isClickToHide')) {
  177. return;
  178. }
  179. this.delayShow();
  180. };
  181. }
  182. break;
  183. case 'custom':
  184. // when trigger type is 'custom', no need to bind eventHandler
  185. // show/hide completely depond on props.visible which change by user
  186. break;
  187. default:
  188. break;
  189. }
  190. return { triggerEventSet, portalEventSet };
  191. }
  192. onResize = () => {
  193. // this.log('resize');
  194. // rePosition when window resize
  195. this.calcPosition();
  196. };
  197. _shouldShow() {
  198. const visible = this.getProp('visible');
  199. if (visible) {
  200. this.show();
  201. } else {
  202. // this.hide();
  203. }
  204. }
  205. delayShow = () => {
  206. const mouseEnterDelay: number = this.getProp('mouseEnterDelay');
  207. this.clearDelayTimer();
  208. if (mouseEnterDelay > 0) {
  209. this._timer = setTimeout(() => {
  210. this.show();
  211. this.clearDelayTimer();
  212. }, mouseEnterDelay);
  213. } else {
  214. this.show();
  215. }
  216. };
  217. show = () => {
  218. const content = this.getProp('content');
  219. const trigger = this.getProp('trigger');
  220. const clickTriggerToHide = this.getProp('clickTriggerToHide');
  221. this.clearDelayTimer();
  222. /**
  223. * If you emit an event in setState callback, you need to place the event listener function before setState to execute.
  224. * This is to avoid event registration being executed later than setState callback when setState is executed in setTimeout.
  225. * internal-issues:1402#note_38969412
  226. */
  227. this._adapter.on('portalInserted', () => {
  228. this.calcPosition();
  229. });
  230. this._adapter.on('positionUpdated', () => {
  231. this._togglePortalVisible(true);
  232. });
  233. const position = this.calcPosition(null, null, null, false);
  234. this._adapter.insertPortal(content, position);
  235. if (trigger === 'custom') {
  236. // eslint-disable-next-line
  237. this._adapter.registerClickOutsideHandler(() => {});
  238. }
  239. /**
  240. * trigger类型是click时,仅当portal被插入显示后,才绑定clickOutsideHandler
  241. * 因为handler需要绑定在document上。如果在constructor阶段绑定
  242. * 当一个页面中有多个容器实例时,一次click会触发多个容器的handler
  243. *
  244. * When the trigger type is click, clickOutsideHandler is bound only after the portal is inserted and displayed
  245. * Because the handler needs to be bound to the document. If you bind during the constructor phase
  246. * When there are multiple container instances in a page, one click triggers the handler of multiple containers
  247. */
  248. if (trigger === 'click' || clickTriggerToHide) {
  249. this._adapter.registerClickOutsideHandler(this.hide);
  250. }
  251. this._bindScrollEvent();
  252. this._bindResizeEvent();
  253. };
  254. _togglePortalVisible(isVisible: boolean) {
  255. const nowVisible = this.getState('visible');
  256. if (nowVisible !== isVisible) {
  257. this._adapter.togglePortalVisible(isVisible, () => this._adapter.notifyVisibleChange(isVisible));
  258. }
  259. }
  260. _roundPixel(pixel: number) {
  261. if (typeof pixel === 'number') {
  262. return Math.round(pixel);
  263. }
  264. return pixel;
  265. }
  266. calcTransformOrigin(position: Position, triggerRect: DOMRect, translateX: number, translateY: number) {
  267. // eslint-disable-next-line
  268. if (position && triggerRect && translateX != null && translateY != null) {
  269. if (this.getProp('transformFromCenter')) {
  270. if (['topLeft', 'bottomLeft'].includes(position)) {
  271. return `${this._roundPixel(triggerRect.width / 2)}px ${-translateY * 100}%`;
  272. }
  273. if (['topRight', 'bottomRight'].includes(position)) {
  274. return `calc(100% - ${this._roundPixel(triggerRect.width / 2)}px) ${-translateY * 100}%`;
  275. }
  276. if (['leftTop', 'rightTop'].includes(position)) {
  277. return `${-translateX * 100}% ${this._roundPixel(triggerRect.height / 2)}px`;
  278. }
  279. if (['leftBottom', 'rightBottom'].includes(position)) {
  280. return `${-translateX * 100}% calc(100% - ${this._roundPixel(triggerRect.height / 2)}px)`;
  281. }
  282. }
  283. return `${-translateX * 100}% ${-translateY * 100}%`;
  284. }
  285. return null;
  286. }
  287. calcPosStyle(triggerRect: DOMRect, wrapperRect: DOMRect, containerRect: PopupContainerDOMRect, position?: Position, spacing?: number) {
  288. triggerRect = (isEmpty(triggerRect) ? triggerRect : this._adapter.getTriggerBounding()) || { ...defaultRect as any };
  289. containerRect = (isEmpty(containerRect) ? containerRect : this._adapter.getPopupContainerRect()) || {
  290. ...defaultRect,
  291. };
  292. wrapperRect = (isEmpty(wrapperRect) ? wrapperRect : this._adapter.getWrapperBounding()) || { ...defaultRect as any };
  293. // eslint-disable-next-line
  294. position = position != null ? position : this.getProp('position');
  295. // eslint-disable-next-line
  296. const SPACING = spacing != null ? spacing : this.getProp('spacing');
  297. const { arrowPointAtCenter, showArrow, arrowBounding } = this.getProps();
  298. const pointAtCenter = showArrow && arrowPointAtCenter;
  299. const horizontalArrowWidth = get(arrowBounding, 'width', 24);
  300. const verticalArrowHeight = get(arrowBounding, 'width', 24);
  301. const arrowOffsetY = get(arrowBounding, 'offsetY', 0);
  302. const positionOffsetX = 6;
  303. const positionOffsetY = 6;
  304. // You must use left/top when rendering, using right/bottom does not render the element position correctly
  305. // Use left/top + translate to achieve tooltip positioning perfectly without knowing the size of the tooltip expansion layer
  306. let left;
  307. let top;
  308. let translateX = 0; // Container x-direction translation distance
  309. let translateY = 0; // Container y-direction translation distance
  310. const middleX = triggerRect.left + triggerRect.width / 2;
  311. const middleY = triggerRect.top + triggerRect.height / 2;
  312. const offsetXWithArrow = positionOffsetX + horizontalArrowWidth / 2;
  313. const offsetYWithArrow = positionOffsetY + verticalArrowHeight / 2;
  314. switch (position) {
  315. case 'top':
  316. left = middleX;
  317. top = triggerRect.top - SPACING;
  318. translateX = -0.5;
  319. translateY = -1;
  320. break;
  321. case 'topLeft':
  322. left = pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left;
  323. top = triggerRect.top - SPACING;
  324. translateY = -1;
  325. break;
  326. case 'topRight':
  327. left = pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right;
  328. top = triggerRect.top - SPACING;
  329. translateY = -1;
  330. translateX = -1;
  331. break;
  332. case 'left':
  333. left = triggerRect.left - SPACING;
  334. top = middleY;
  335. translateX = -1;
  336. translateY = -0.5;
  337. break;
  338. case 'leftTop':
  339. left = triggerRect.left - SPACING;
  340. top = pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top;
  341. translateX = -1;
  342. break;
  343. case 'leftBottom':
  344. left = triggerRect.left - SPACING;
  345. top = pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom;
  346. translateX = -1;
  347. translateY = -1;
  348. break;
  349. case 'bottom':
  350. left = middleX;
  351. top = triggerRect.top + triggerRect.height + SPACING;
  352. translateX = -0.5;
  353. break;
  354. case 'bottomLeft':
  355. left = pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left;
  356. top = triggerRect.bottom + SPACING;
  357. break;
  358. case 'bottomRight':
  359. left = pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right;
  360. top = triggerRect.bottom + SPACING;
  361. translateX = -1;
  362. break;
  363. case 'right':
  364. left = triggerRect.right + SPACING;
  365. top = middleY;
  366. translateY = -0.5;
  367. break;
  368. case 'rightTop':
  369. left = triggerRect.right + SPACING;
  370. top = pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top;
  371. break;
  372. case 'rightBottom':
  373. left = triggerRect.right + SPACING;
  374. top = pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom;
  375. translateY = -1;
  376. break;
  377. case 'leftTopOver':
  378. left = triggerRect.left;
  379. top = triggerRect.top;
  380. break;
  381. case 'rightTopOver':
  382. left = triggerRect.right;
  383. top = triggerRect.top;
  384. translateX = -1;
  385. break;
  386. default:
  387. break;
  388. }
  389. const transformOrigin = this.calcTransformOrigin(position, triggerRect, translateX, translateY); // Transform origin
  390. const _containerIsBody = this._adapter.containerIsBody();
  391. // Calculate container positioning relative to window
  392. left = left - containerRect.left;
  393. top = top - containerRect.top;
  394. /**
  395. * container为body时,如果position不为relative或absolute,这时trigger计算出的top/left会根据html定位(initial containing block)
  396. * 此时如果body有margin,则计算出的位置相对于body会有问题 fix issue #1368
  397. *
  398. * When container is body, if position is not relative or absolute, then the top/left calculated by trigger will be positioned according to html
  399. * At this time, if the body has a margin, the calculated position will have a problem relative to the body fix issue #1368
  400. */
  401. if (_containerIsBody && !this._adapter.containerIsRelativeOrAbsolute()) {
  402. const documentEleRect = this._adapter.getDocumentElementBounding();
  403. // Represents the left of the body relative to html
  404. left += containerRect.left - documentEleRect.left;
  405. // Represents the top of the body relative to html
  406. top += containerRect.top - documentEleRect.top;
  407. }
  408. // ContainerRect.scrollLeft to solve the inner scrolling of the container
  409. left = _containerIsBody ? left : left + containerRect.scrollLeft;
  410. top = _containerIsBody ? top : top + containerRect.scrollTop;
  411. const triggerHeight = triggerRect.height;
  412. if (
  413. this.getProp('showArrow') &&
  414. !arrowPointAtCenter &&
  415. triggerHeight <= (verticalArrowHeight / 2 + arrowOffsetY) * 2
  416. ) {
  417. const offsetY = triggerHeight / 2 - (arrowOffsetY + verticalArrowHeight / 2);
  418. if ((position.includes('Top') || position.includes('Bottom')) && !position.includes('Over')) {
  419. top = position.includes('Top') ? top + offsetY : top - offsetY;
  420. }
  421. }
  422. // The left/top value here must be rounded, otherwise it will cause the small triangle to shake
  423. const style: Record<string, string | number> = {
  424. left: this._roundPixel(left),
  425. top: this._roundPixel(top),
  426. };
  427. let transform = '';
  428. // eslint-disable-next-line
  429. if (translateX != null) {
  430. transform += `translateX(${translateX * 100}%) `;
  431. Object.defineProperty(style, 'translateX', {
  432. enumerable: false,
  433. value: translateX,
  434. });
  435. }
  436. // eslint-disable-next-line
  437. if (translateY != null) {
  438. transform += `translateY(${translateY * 100}%) `;
  439. Object.defineProperty(style, 'translateY', {
  440. enumerable: false,
  441. value: translateY,
  442. });
  443. }
  444. // eslint-disable-next-line
  445. if (transformOrigin != null) {
  446. style.transformOrigin = transformOrigin;
  447. }
  448. if (transform) {
  449. style.transform = transform;
  450. }
  451. return style;
  452. }
  453. /**
  454. * 耦合的东西比较多,稍微罗列一下:
  455. *
  456. * - 根据 trigger 和 wrapper 的 boundingClient 计算当前的 left、top、transform-origin
  457. * - 根据当前的 position 和 wrapper 的 boundingClient 决定是否需要自动调整位置
  458. * - 根据当前的 position、trigger 的 boundingClient 以及 motion.handleStyle 调整当前的 style
  459. *
  460. * There are many coupling things, a little list:
  461. *
  462. * - calculate the current left, top, and transfer-origin according to the boundingClient of trigger and wrapper
  463. * - decide whether to automatically adjust the position according to the current position and the boundingClient of wrapper
  464. * - adjust the current style according to the current position, the boundingClient of trigger and motion.handle Style
  465. */
  466. calcPosition = (triggerRect?: DOMRect, wrapperRect?: DOMRect, containerRect?: PopupContainerDOMRect, shouldUpdatePos = true) => {
  467. triggerRect = (isEmpty(triggerRect) ? this._adapter.getTriggerBounding() : triggerRect) || { ...defaultRect as any };
  468. containerRect = (isEmpty(containerRect) ? this._adapter.getPopupContainerRect() : containerRect) || {
  469. ...defaultRect,
  470. };
  471. wrapperRect = (isEmpty(wrapperRect) ? this._adapter.getWrapperBounding() : wrapperRect) || { ...defaultRect as any };
  472. // console.log('containerRect: ', containerRect, 'triggerRect: ', triggerRect, 'wrapperRect: ', wrapperRect);
  473. let style = this.calcPosStyle(triggerRect, wrapperRect, containerRect);
  474. let position = this.getProp('position');
  475. if (this.getProp('autoAdjustOverflow')) {
  476. // console.log('style: ', style, '\ntriggerRect: ', triggerRect, '\nwrapperRect: ', wrapperRect);
  477. const adjustedPos = this.adjustPosIfNeed(position, style, triggerRect, wrapperRect, containerRect);
  478. if (position !== adjustedPos) {
  479. position = adjustedPos;
  480. style = this.calcPosStyle(triggerRect, wrapperRect, containerRect, position);
  481. }
  482. }
  483. if (shouldUpdatePos && this._mounted) {
  484. // this._adapter.updatePlacementAttr(style.position);
  485. this._adapter.setPosition({ ...style, position });
  486. }
  487. return style;
  488. };
  489. isLR(position = '') {
  490. return position.indexOf('left') === 0 || position.indexOf('right') === 0;
  491. }
  492. isTB(position = '') {
  493. return position.indexOf('top') === 0 || position.indexOf('bottom') === 0;
  494. }
  495. // place the dom correctly
  496. adjustPosIfNeed(position: Position | string, style: Record<string, any>, triggerRect: DOMRect, wrapperRect: DOMRect, containerRect: PopupContainerDOMRect) {
  497. const { innerWidth, innerHeight } = window;
  498. const { spacing } = this.getProps();
  499. if (wrapperRect.width > 0 && wrapperRect.height > 0) {
  500. // let clientLeft = left + translateX * wrapperRect.width - containerRect.scrollLeft;
  501. // let clientTop = top + translateY * wrapperRect.height - containerRect.scrollTop;
  502. // if (this._adapter.containerIsBody() || this._adapter.containerIsRelative()) {
  503. // clientLeft += containerRect.left;
  504. // clientTop += containerRect.top;
  505. // }
  506. // const clientRight = clientLeft + wrapperRect.width;
  507. // const clientBottom = clientTop + wrapperRect.height;
  508. // The relative position of the elements on the screen
  509. // https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/tooltip-pic.svg
  510. const clientLeft = triggerRect.left;
  511. const clientRight = triggerRect.right;
  512. const clientTop = triggerRect.top;
  513. const clientBottom = triggerRect.bottom;
  514. const restClientLeft = innerWidth - clientLeft;
  515. const restClientTop = innerHeight - clientTop;
  516. const restClientRight = innerWidth - clientRight;
  517. const restClientBottom = innerHeight - clientBottom;
  518. const widthIsBigger = wrapperRect.width > triggerRect.width;
  519. const heightIsBigger = wrapperRect.height > triggerRect.height;
  520. // The wrapperR ect.top|bottom equivalent cannot be directly used here for comparison, which is easy to cause jitter
  521. const shouldReverseTop = clientTop < wrapperRect.height + spacing && restClientBottom > wrapperRect.height + spacing;
  522. const shouldReverseLeft = clientLeft < wrapperRect.width + spacing && restClientRight > wrapperRect.width + spacing;
  523. const sholdReverseBottom = restClientBottom < wrapperRect.height + spacing && clientTop > wrapperRect.height + spacing;
  524. const shouldReverseRight = restClientRight < wrapperRect.width + spacing && clientLeft > wrapperRect.width + spacing;
  525. const shouldReverseTopSide = restClientTop < wrapperRect.height && clientBottom > wrapperRect.height;
  526. const shouldReverseBottomSide = clientBottom < wrapperRect.height && restClientTop > wrapperRect.height;
  527. const shouldReverseLeftSide = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
  528. const shouldReverseRightSide = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
  529. switch (position) {
  530. case 'top':
  531. if (shouldReverseTop) {
  532. position = this._reversePos(position, true);
  533. }
  534. break;
  535. case 'topLeft':
  536. if (shouldReverseTop) {
  537. position = this._reversePos(position, true);
  538. }
  539. if (shouldReverseLeftSide && widthIsBigger) {
  540. position = this._reversePos(position);
  541. }
  542. break;
  543. case 'topRight':
  544. if (shouldReverseTop) {
  545. position = this._reversePos(position, true);
  546. }
  547. if (shouldReverseRightSide && widthIsBigger) {
  548. position = this._reversePos(position);
  549. }
  550. break;
  551. case 'left':
  552. if (shouldReverseLeft) {
  553. position = this._reversePos(position);
  554. }
  555. break;
  556. case 'leftTop':
  557. if (shouldReverseLeft) {
  558. position = this._reversePos(position);
  559. }
  560. if (shouldReverseTopSide && heightIsBigger) {
  561. position = this._reversePos(position, true);
  562. }
  563. break;
  564. case 'leftBottom':
  565. if (shouldReverseLeft) {
  566. position = this._reversePos(position);
  567. }
  568. if (shouldReverseBottomSide && heightIsBigger) {
  569. position = this._reversePos(position, true);
  570. }
  571. break;
  572. case 'bottom':
  573. if (sholdReverseBottom) {
  574. position = this._reversePos(position, true);
  575. }
  576. break;
  577. case 'bottomLeft':
  578. if (sholdReverseBottom) {
  579. position = this._reversePos(position, true);
  580. }
  581. if (shouldReverseLeftSide && widthIsBigger) {
  582. position = this._reversePos(position);
  583. }
  584. break;
  585. case 'bottomRight':
  586. if (sholdReverseBottom) {
  587. position = this._reversePos(position, true);
  588. }
  589. if (shouldReverseRightSide && widthIsBigger) {
  590. position = this._reversePos(position);
  591. }
  592. break;
  593. case 'right':
  594. if (shouldReverseRight) {
  595. position = this._reversePos(position);
  596. }
  597. break;
  598. case 'rightTop':
  599. if (shouldReverseRight) {
  600. position = this._reversePos(position);
  601. }
  602. if (shouldReverseTopSide && heightIsBigger) {
  603. position = this._reversePos(position, true);
  604. }
  605. break;
  606. case 'rightBottom':
  607. if (shouldReverseRight) {
  608. position = this._reversePos(position);
  609. }
  610. if (shouldReverseBottomSide && heightIsBigger) {
  611. position = this._reversePos(position, true);
  612. }
  613. break;
  614. default:
  615. break;
  616. }
  617. }
  618. return position;
  619. }
  620. delayHide = () => {
  621. const mouseLeaveDelay = this.getProp('mouseLeaveDelay');
  622. this.clearDelayTimer();
  623. if (mouseLeaveDelay > 0) {
  624. this._timer = setTimeout(() => {
  625. // console.log('delayHide for ', mouseLeaveDelay, ' ms, ', ...args);
  626. this.hide();
  627. this.clearDelayTimer();
  628. }, mouseLeaveDelay);
  629. } else {
  630. this.hide();
  631. }
  632. };
  633. hide = () => {
  634. this.clearDelayTimer();
  635. this._togglePortalVisible(false);
  636. this._adapter.off('portalInserted');
  637. this._adapter.off('positionUpdated');
  638. if (!this._adapter.canMotion()) {
  639. this._adapter.removePortal();
  640. // When the portal is removed, the global click outside event binding is also removed
  641. this._adapter.unregisterClickOutsideHandler();
  642. this._unBindScrollEvent();
  643. this._unBindResizeEvent();
  644. }
  645. };
  646. _bindScrollEvent() {
  647. this._adapter.registerScrollHandler(() => this.calcPosition());
  648. // Capture scroll events on the window to determine whether the current scrolling area (e.target) will affect the positioning of the pop-up layer relative to the viewport when scrolling
  649. // (By determining whether the e.target contains the triggerDom of the current tooltip) If so, the pop-up layer will also be affected and needs to be repositioned
  650. }
  651. _unBindScrollEvent() {
  652. this._adapter.unregisterScrollHandler();
  653. }
  654. _initContainerPosition() {
  655. this._adapter.updateContainerPosition();
  656. }
  657. }