floating-ui.core.mjs 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. import { getSideAxis, getAlignmentAxis, getAxisLength, getSide, getAlignment, evaluate, getPaddingObject, rectToClientRect, min, clamp, placements, getAlignmentSides, getOppositeAlignmentPlacement, getOppositePlacement, getExpandedPlacements, getOppositeAxisPlacements, sides, max, getOppositeAxis } from '@floating-ui/utils';
  2. export { rectToClientRect } from '@floating-ui/utils';
  3. function computeCoordsFromPlacement(_ref, placement, rtl) {
  4. let {
  5. reference,
  6. floating
  7. } = _ref;
  8. const sideAxis = getSideAxis(placement);
  9. const alignmentAxis = getAlignmentAxis(placement);
  10. const alignLength = getAxisLength(alignmentAxis);
  11. const side = getSide(placement);
  12. const isVertical = sideAxis === 'y';
  13. const commonX = reference.x + reference.width / 2 - floating.width / 2;
  14. const commonY = reference.y + reference.height / 2 - floating.height / 2;
  15. const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2;
  16. let coords;
  17. switch (side) {
  18. case 'top':
  19. coords = {
  20. x: commonX,
  21. y: reference.y - floating.height
  22. };
  23. break;
  24. case 'bottom':
  25. coords = {
  26. x: commonX,
  27. y: reference.y + reference.height
  28. };
  29. break;
  30. case 'right':
  31. coords = {
  32. x: reference.x + reference.width,
  33. y: commonY
  34. };
  35. break;
  36. case 'left':
  37. coords = {
  38. x: reference.x - floating.width,
  39. y: commonY
  40. };
  41. break;
  42. default:
  43. coords = {
  44. x: reference.x,
  45. y: reference.y
  46. };
  47. }
  48. switch (getAlignment(placement)) {
  49. case 'start':
  50. coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
  51. break;
  52. case 'end':
  53. coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
  54. break;
  55. }
  56. return coords;
  57. }
  58. /**
  59. * Computes the `x` and `y` coordinates that will place the floating element
  60. * next to a given reference element.
  61. *
  62. * This export does not have any `platform` interface logic. You will need to
  63. * write one for the platform you are using Floating UI with.
  64. */
  65. const computePosition = async (reference, floating, config) => {
  66. const {
  67. placement = 'bottom',
  68. strategy = 'absolute',
  69. middleware = [],
  70. platform
  71. } = config;
  72. const validMiddleware = middleware.filter(Boolean);
  73. const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating));
  74. let rects = await platform.getElementRects({
  75. reference,
  76. floating,
  77. strategy
  78. });
  79. let {
  80. x,
  81. y
  82. } = computeCoordsFromPlacement(rects, placement, rtl);
  83. let statefulPlacement = placement;
  84. let middlewareData = {};
  85. let resetCount = 0;
  86. for (let i = 0; i < validMiddleware.length; i++) {
  87. const {
  88. name,
  89. fn
  90. } = validMiddleware[i];
  91. const {
  92. x: nextX,
  93. y: nextY,
  94. data,
  95. reset
  96. } = await fn({
  97. x,
  98. y,
  99. initialPlacement: placement,
  100. placement: statefulPlacement,
  101. strategy,
  102. middlewareData,
  103. rects,
  104. platform,
  105. elements: {
  106. reference,
  107. floating
  108. }
  109. });
  110. x = nextX != null ? nextX : x;
  111. y = nextY != null ? nextY : y;
  112. middlewareData = {
  113. ...middlewareData,
  114. [name]: {
  115. ...middlewareData[name],
  116. ...data
  117. }
  118. };
  119. if (reset && resetCount <= 50) {
  120. resetCount++;
  121. if (typeof reset === 'object') {
  122. if (reset.placement) {
  123. statefulPlacement = reset.placement;
  124. }
  125. if (reset.rects) {
  126. rects = reset.rects === true ? await platform.getElementRects({
  127. reference,
  128. floating,
  129. strategy
  130. }) : reset.rects;
  131. }
  132. ({
  133. x,
  134. y
  135. } = computeCoordsFromPlacement(rects, statefulPlacement, rtl));
  136. }
  137. i = -1;
  138. }
  139. }
  140. return {
  141. x,
  142. y,
  143. placement: statefulPlacement,
  144. strategy,
  145. middlewareData
  146. };
  147. };
  148. /**
  149. * Resolves with an object of overflow side offsets that determine how much the
  150. * element is overflowing a given clipping boundary on each side.
  151. * - positive = overflowing the boundary by that number of pixels
  152. * - negative = how many pixels left before it will overflow
  153. * - 0 = lies flush with the boundary
  154. * @see https://floating-ui.com/docs/detectOverflow
  155. */
  156. async function detectOverflow(state, options) {
  157. var _await$platform$isEle;
  158. if (options === void 0) {
  159. options = {};
  160. }
  161. const {
  162. x,
  163. y,
  164. platform,
  165. rects,
  166. elements,
  167. strategy
  168. } = state;
  169. const {
  170. boundary = 'clippingAncestors',
  171. rootBoundary = 'viewport',
  172. elementContext = 'floating',
  173. altBoundary = false,
  174. padding = 0
  175. } = evaluate(options, state);
  176. const paddingObject = getPaddingObject(padding);
  177. const altContext = elementContext === 'floating' ? 'reference' : 'floating';
  178. const element = elements[altBoundary ? altContext : elementContext];
  179. const clippingClientRect = rectToClientRect(await platform.getClippingRect({
  180. element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))),
  181. boundary,
  182. rootBoundary,
  183. strategy
  184. }));
  185. const rect = elementContext === 'floating' ? {
  186. ...rects.floating,
  187. x,
  188. y
  189. } : rects.reference;
  190. const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating));
  191. const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || {
  192. x: 1,
  193. y: 1
  194. } : {
  195. x: 1,
  196. y: 1
  197. };
  198. const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({
  199. elements,
  200. rect,
  201. offsetParent,
  202. strategy
  203. }) : rect);
  204. return {
  205. top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y,
  206. bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y,
  207. left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x,
  208. right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x
  209. };
  210. }
  211. /**
  212. * Provides data to position an inner element of the floating element so that it
  213. * appears centered to the reference element.
  214. * @see https://floating-ui.com/docs/arrow
  215. */
  216. const arrow = options => ({
  217. name: 'arrow',
  218. options,
  219. async fn(state) {
  220. const {
  221. x,
  222. y,
  223. placement,
  224. rects,
  225. platform,
  226. elements,
  227. middlewareData
  228. } = state;
  229. // Since `element` is required, we don't Partial<> the type.
  230. const {
  231. element,
  232. padding = 0
  233. } = evaluate(options, state) || {};
  234. if (element == null) {
  235. return {};
  236. }
  237. const paddingObject = getPaddingObject(padding);
  238. const coords = {
  239. x,
  240. y
  241. };
  242. const axis = getAlignmentAxis(placement);
  243. const length = getAxisLength(axis);
  244. const arrowDimensions = await platform.getDimensions(element);
  245. const isYAxis = axis === 'y';
  246. const minProp = isYAxis ? 'top' : 'left';
  247. const maxProp = isYAxis ? 'bottom' : 'right';
  248. const clientProp = isYAxis ? 'clientHeight' : 'clientWidth';
  249. const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length];
  250. const startDiff = coords[axis] - rects.reference[axis];
  251. const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element));
  252. let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0;
  253. // DOM platform can return `window` as the `offsetParent`.
  254. if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) {
  255. clientSize = elements.floating[clientProp] || rects.floating[length];
  256. }
  257. const centerToReference = endDiff / 2 - startDiff / 2;
  258. // If the padding is large enough that it causes the arrow to no longer be
  259. // centered, modify the padding so that it is centered.
  260. const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1;
  261. const minPadding = min(paddingObject[minProp], largestPossiblePadding);
  262. const maxPadding = min(paddingObject[maxProp], largestPossiblePadding);
  263. // Make sure the arrow doesn't overflow the floating element if the center
  264. // point is outside the floating element's bounds.
  265. const min$1 = minPadding;
  266. const max = clientSize - arrowDimensions[length] - maxPadding;
  267. const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
  268. const offset = clamp(min$1, center, max);
  269. // If the reference is small enough that the arrow's padding causes it to
  270. // to point to nothing for an aligned placement, adjust the offset of the
  271. // floating element itself. To ensure `shift()` continues to take action,
  272. // a single reset is performed when this is true.
  273. const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;
  274. const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0;
  275. return {
  276. [axis]: coords[axis] + alignmentOffset,
  277. data: {
  278. [axis]: offset,
  279. centerOffset: center - offset - alignmentOffset,
  280. ...(shouldAddOffset && {
  281. alignmentOffset
  282. })
  283. },
  284. reset: shouldAddOffset
  285. };
  286. }
  287. });
  288. function getPlacementList(alignment, autoAlignment, allowedPlacements) {
  289. const allowedPlacementsSortedByAlignment = alignment ? [...allowedPlacements.filter(placement => getAlignment(placement) === alignment), ...allowedPlacements.filter(placement => getAlignment(placement) !== alignment)] : allowedPlacements.filter(placement => getSide(placement) === placement);
  290. return allowedPlacementsSortedByAlignment.filter(placement => {
  291. if (alignment) {
  292. return getAlignment(placement) === alignment || (autoAlignment ? getOppositeAlignmentPlacement(placement) !== placement : false);
  293. }
  294. return true;
  295. });
  296. }
  297. /**
  298. * Optimizes the visibility of the floating element by choosing the placement
  299. * that has the most space available automatically, without needing to specify a
  300. * preferred placement. Alternative to `flip`.
  301. * @see https://floating-ui.com/docs/autoPlacement
  302. */
  303. const autoPlacement = function (options) {
  304. if (options === void 0) {
  305. options = {};
  306. }
  307. return {
  308. name: 'autoPlacement',
  309. options,
  310. async fn(state) {
  311. var _middlewareData$autoP, _middlewareData$autoP2, _placementsThatFitOnE;
  312. const {
  313. rects,
  314. middlewareData,
  315. placement,
  316. platform,
  317. elements
  318. } = state;
  319. const {
  320. crossAxis = false,
  321. alignment,
  322. allowedPlacements = placements,
  323. autoAlignment = true,
  324. ...detectOverflowOptions
  325. } = evaluate(options, state);
  326. const placements$1 = alignment !== undefined || allowedPlacements === placements ? getPlacementList(alignment || null, autoAlignment, allowedPlacements) : allowedPlacements;
  327. const overflow = await detectOverflow(state, detectOverflowOptions);
  328. const currentIndex = ((_middlewareData$autoP = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP.index) || 0;
  329. const currentPlacement = placements$1[currentIndex];
  330. if (currentPlacement == null) {
  331. return {};
  332. }
  333. const alignmentSides = getAlignmentSides(currentPlacement, rects, await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)));
  334. // Make `computeCoords` start from the right place.
  335. if (placement !== currentPlacement) {
  336. return {
  337. reset: {
  338. placement: placements$1[0]
  339. }
  340. };
  341. }
  342. const currentOverflows = [overflow[getSide(currentPlacement)], overflow[alignmentSides[0]], overflow[alignmentSides[1]]];
  343. const allOverflows = [...(((_middlewareData$autoP2 = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP2.overflows) || []), {
  344. placement: currentPlacement,
  345. overflows: currentOverflows
  346. }];
  347. const nextPlacement = placements$1[currentIndex + 1];
  348. // There are more placements to check.
  349. if (nextPlacement) {
  350. return {
  351. data: {
  352. index: currentIndex + 1,
  353. overflows: allOverflows
  354. },
  355. reset: {
  356. placement: nextPlacement
  357. }
  358. };
  359. }
  360. const placementsSortedByMostSpace = allOverflows.map(d => {
  361. const alignment = getAlignment(d.placement);
  362. return [d.placement, alignment && crossAxis ?
  363. // Check along the mainAxis and main crossAxis side.
  364. d.overflows.slice(0, 2).reduce((acc, v) => acc + v, 0) :
  365. // Check only the mainAxis.
  366. d.overflows[0], d.overflows];
  367. }).sort((a, b) => a[1] - b[1]);
  368. const placementsThatFitOnEachSide = placementsSortedByMostSpace.filter(d => d[2].slice(0,
  369. // Aligned placements should not check their opposite crossAxis
  370. // side.
  371. getAlignment(d[0]) ? 2 : 3).every(v => v <= 0));
  372. const resetPlacement = ((_placementsThatFitOnE = placementsThatFitOnEachSide[0]) == null ? void 0 : _placementsThatFitOnE[0]) || placementsSortedByMostSpace[0][0];
  373. if (resetPlacement !== placement) {
  374. return {
  375. data: {
  376. index: currentIndex + 1,
  377. overflows: allOverflows
  378. },
  379. reset: {
  380. placement: resetPlacement
  381. }
  382. };
  383. }
  384. return {};
  385. }
  386. };
  387. };
  388. /**
  389. * Optimizes the visibility of the floating element by flipping the `placement`
  390. * in order to keep it in view when the preferred placement(s) will overflow the
  391. * clipping boundary. Alternative to `autoPlacement`.
  392. * @see https://floating-ui.com/docs/flip
  393. */
  394. const flip = function (options) {
  395. if (options === void 0) {
  396. options = {};
  397. }
  398. return {
  399. name: 'flip',
  400. options,
  401. async fn(state) {
  402. var _middlewareData$arrow, _middlewareData$flip;
  403. const {
  404. placement,
  405. middlewareData,
  406. rects,
  407. initialPlacement,
  408. platform,
  409. elements
  410. } = state;
  411. const {
  412. mainAxis: checkMainAxis = true,
  413. crossAxis: checkCrossAxis = true,
  414. fallbackPlacements: specifiedFallbackPlacements,
  415. fallbackStrategy = 'bestFit',
  416. fallbackAxisSideDirection = 'none',
  417. flipAlignment = true,
  418. ...detectOverflowOptions
  419. } = evaluate(options, state);
  420. // If a reset by the arrow was caused due to an alignment offset being
  421. // added, we should skip any logic now since `flip()` has already done its
  422. // work.
  423. // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643
  424. if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
  425. return {};
  426. }
  427. const side = getSide(placement);
  428. const isBasePlacement = getSide(initialPlacement) === initialPlacement;
  429. const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));
  430. const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement));
  431. if (!specifiedFallbackPlacements && fallbackAxisSideDirection !== 'none') {
  432. fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl));
  433. }
  434. const placements = [initialPlacement, ...fallbackPlacements];
  435. const overflow = await detectOverflow(state, detectOverflowOptions);
  436. const overflows = [];
  437. let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || [];
  438. if (checkMainAxis) {
  439. overflows.push(overflow[side]);
  440. }
  441. if (checkCrossAxis) {
  442. const sides = getAlignmentSides(placement, rects, rtl);
  443. overflows.push(overflow[sides[0]], overflow[sides[1]]);
  444. }
  445. overflowsData = [...overflowsData, {
  446. placement,
  447. overflows
  448. }];
  449. // One or more sides is overflowing.
  450. if (!overflows.every(side => side <= 0)) {
  451. var _middlewareData$flip2, _overflowsData$filter;
  452. const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1;
  453. const nextPlacement = placements[nextIndex];
  454. if (nextPlacement) {
  455. // Try next placement and re-run the lifecycle.
  456. return {
  457. data: {
  458. index: nextIndex,
  459. overflows: overflowsData
  460. },
  461. reset: {
  462. placement: nextPlacement
  463. }
  464. };
  465. }
  466. // First, find the candidates that fit on the mainAxis side of overflow,
  467. // then find the placement that fits the best on the main crossAxis side.
  468. let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement;
  469. // Otherwise fallback.
  470. if (!resetPlacement) {
  471. switch (fallbackStrategy) {
  472. case 'bestFit':
  473. {
  474. var _overflowsData$map$so;
  475. const placement = (_overflowsData$map$so = overflowsData.map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$map$so[0];
  476. if (placement) {
  477. resetPlacement = placement;
  478. }
  479. break;
  480. }
  481. case 'initialPlacement':
  482. resetPlacement = initialPlacement;
  483. break;
  484. }
  485. }
  486. if (placement !== resetPlacement) {
  487. return {
  488. reset: {
  489. placement: resetPlacement
  490. }
  491. };
  492. }
  493. }
  494. return {};
  495. }
  496. };
  497. };
  498. function getSideOffsets(overflow, rect) {
  499. return {
  500. top: overflow.top - rect.height,
  501. right: overflow.right - rect.width,
  502. bottom: overflow.bottom - rect.height,
  503. left: overflow.left - rect.width
  504. };
  505. }
  506. function isAnySideFullyClipped(overflow) {
  507. return sides.some(side => overflow[side] >= 0);
  508. }
  509. /**
  510. * Provides data to hide the floating element in applicable situations, such as
  511. * when it is not in the same clipping context as the reference element.
  512. * @see https://floating-ui.com/docs/hide
  513. */
  514. const hide = function (options) {
  515. if (options === void 0) {
  516. options = {};
  517. }
  518. return {
  519. name: 'hide',
  520. options,
  521. async fn(state) {
  522. const {
  523. rects
  524. } = state;
  525. const {
  526. strategy = 'referenceHidden',
  527. ...detectOverflowOptions
  528. } = evaluate(options, state);
  529. switch (strategy) {
  530. case 'referenceHidden':
  531. {
  532. const overflow = await detectOverflow(state, {
  533. ...detectOverflowOptions,
  534. elementContext: 'reference'
  535. });
  536. const offsets = getSideOffsets(overflow, rects.reference);
  537. return {
  538. data: {
  539. referenceHiddenOffsets: offsets,
  540. referenceHidden: isAnySideFullyClipped(offsets)
  541. }
  542. };
  543. }
  544. case 'escaped':
  545. {
  546. const overflow = await detectOverflow(state, {
  547. ...detectOverflowOptions,
  548. altBoundary: true
  549. });
  550. const offsets = getSideOffsets(overflow, rects.floating);
  551. return {
  552. data: {
  553. escapedOffsets: offsets,
  554. escaped: isAnySideFullyClipped(offsets)
  555. }
  556. };
  557. }
  558. default:
  559. {
  560. return {};
  561. }
  562. }
  563. }
  564. };
  565. };
  566. function getBoundingRect(rects) {
  567. const minX = min(...rects.map(rect => rect.left));
  568. const minY = min(...rects.map(rect => rect.top));
  569. const maxX = max(...rects.map(rect => rect.right));
  570. const maxY = max(...rects.map(rect => rect.bottom));
  571. return {
  572. x: minX,
  573. y: minY,
  574. width: maxX - minX,
  575. height: maxY - minY
  576. };
  577. }
  578. function getRectsByLine(rects) {
  579. const sortedRects = rects.slice().sort((a, b) => a.y - b.y);
  580. const groups = [];
  581. let prevRect = null;
  582. for (let i = 0; i < sortedRects.length; i++) {
  583. const rect = sortedRects[i];
  584. if (!prevRect || rect.y - prevRect.y > prevRect.height / 2) {
  585. groups.push([rect]);
  586. } else {
  587. groups[groups.length - 1].push(rect);
  588. }
  589. prevRect = rect;
  590. }
  591. return groups.map(rect => rectToClientRect(getBoundingRect(rect)));
  592. }
  593. /**
  594. * Provides improved positioning for inline reference elements that can span
  595. * over multiple lines, such as hyperlinks or range selections.
  596. * @see https://floating-ui.com/docs/inline
  597. */
  598. const inline = function (options) {
  599. if (options === void 0) {
  600. options = {};
  601. }
  602. return {
  603. name: 'inline',
  604. options,
  605. async fn(state) {
  606. const {
  607. placement,
  608. elements,
  609. rects,
  610. platform,
  611. strategy
  612. } = state;
  613. // A MouseEvent's client{X,Y} coords can be up to 2 pixels off a
  614. // ClientRect's bounds, despite the event listener being triggered. A
  615. // padding of 2 seems to handle this issue.
  616. const {
  617. padding = 2,
  618. x,
  619. y
  620. } = evaluate(options, state);
  621. const nativeClientRects = Array.from((await (platform.getClientRects == null ? void 0 : platform.getClientRects(elements.reference))) || []);
  622. const clientRects = getRectsByLine(nativeClientRects);
  623. const fallback = rectToClientRect(getBoundingRect(nativeClientRects));
  624. const paddingObject = getPaddingObject(padding);
  625. function getBoundingClientRect() {
  626. // There are two rects and they are disjoined.
  627. if (clientRects.length === 2 && clientRects[0].left > clientRects[1].right && x != null && y != null) {
  628. // Find the first rect in which the point is fully inside.
  629. return clientRects.find(rect => x > rect.left - paddingObject.left && x < rect.right + paddingObject.right && y > rect.top - paddingObject.top && y < rect.bottom + paddingObject.bottom) || fallback;
  630. }
  631. // There are 2 or more connected rects.
  632. if (clientRects.length >= 2) {
  633. if (getSideAxis(placement) === 'y') {
  634. const firstRect = clientRects[0];
  635. const lastRect = clientRects[clientRects.length - 1];
  636. const isTop = getSide(placement) === 'top';
  637. const top = firstRect.top;
  638. const bottom = lastRect.bottom;
  639. const left = isTop ? firstRect.left : lastRect.left;
  640. const right = isTop ? firstRect.right : lastRect.right;
  641. const width = right - left;
  642. const height = bottom - top;
  643. return {
  644. top,
  645. bottom,
  646. left,
  647. right,
  648. width,
  649. height,
  650. x: left,
  651. y: top
  652. };
  653. }
  654. const isLeftSide = getSide(placement) === 'left';
  655. const maxRight = max(...clientRects.map(rect => rect.right));
  656. const minLeft = min(...clientRects.map(rect => rect.left));
  657. const measureRects = clientRects.filter(rect => isLeftSide ? rect.left === minLeft : rect.right === maxRight);
  658. const top = measureRects[0].top;
  659. const bottom = measureRects[measureRects.length - 1].bottom;
  660. const left = minLeft;
  661. const right = maxRight;
  662. const width = right - left;
  663. const height = bottom - top;
  664. return {
  665. top,
  666. bottom,
  667. left,
  668. right,
  669. width,
  670. height,
  671. x: left,
  672. y: top
  673. };
  674. }
  675. return fallback;
  676. }
  677. const resetRects = await platform.getElementRects({
  678. reference: {
  679. getBoundingClientRect
  680. },
  681. floating: elements.floating,
  682. strategy
  683. });
  684. if (rects.reference.x !== resetRects.reference.x || rects.reference.y !== resetRects.reference.y || rects.reference.width !== resetRects.reference.width || rects.reference.height !== resetRects.reference.height) {
  685. return {
  686. reset: {
  687. rects: resetRects
  688. }
  689. };
  690. }
  691. return {};
  692. }
  693. };
  694. };
  695. // For type backwards-compatibility, the `OffsetOptions` type was also
  696. // Derivable.
  697. async function convertValueToCoords(state, options) {
  698. const {
  699. placement,
  700. platform,
  701. elements
  702. } = state;
  703. const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));
  704. const side = getSide(placement);
  705. const alignment = getAlignment(placement);
  706. const isVertical = getSideAxis(placement) === 'y';
  707. const mainAxisMulti = ['left', 'top'].includes(side) ? -1 : 1;
  708. const crossAxisMulti = rtl && isVertical ? -1 : 1;
  709. const rawValue = evaluate(options, state);
  710. let {
  711. mainAxis,
  712. crossAxis,
  713. alignmentAxis
  714. } = typeof rawValue === 'number' ? {
  715. mainAxis: rawValue,
  716. crossAxis: 0,
  717. alignmentAxis: null
  718. } : {
  719. mainAxis: 0,
  720. crossAxis: 0,
  721. alignmentAxis: null,
  722. ...rawValue
  723. };
  724. if (alignment && typeof alignmentAxis === 'number') {
  725. crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis;
  726. }
  727. return isVertical ? {
  728. x: crossAxis * crossAxisMulti,
  729. y: mainAxis * mainAxisMulti
  730. } : {
  731. x: mainAxis * mainAxisMulti,
  732. y: crossAxis * crossAxisMulti
  733. };
  734. }
  735. /**
  736. * Modifies the placement by translating the floating element along the
  737. * specified axes.
  738. * A number (shorthand for `mainAxis` or distance), or an axes configuration
  739. * object may be passed.
  740. * @see https://floating-ui.com/docs/offset
  741. */
  742. const offset = function (options) {
  743. if (options === void 0) {
  744. options = 0;
  745. }
  746. return {
  747. name: 'offset',
  748. options,
  749. async fn(state) {
  750. var _middlewareData$offse, _middlewareData$arrow;
  751. const {
  752. x,
  753. y,
  754. placement,
  755. middlewareData
  756. } = state;
  757. const diffCoords = await convertValueToCoords(state, options);
  758. // If the placement is the same and the arrow caused an alignment offset
  759. // then we don't need to change the positioning coordinates.
  760. if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
  761. return {};
  762. }
  763. return {
  764. x: x + diffCoords.x,
  765. y: y + diffCoords.y,
  766. data: {
  767. ...diffCoords,
  768. placement
  769. }
  770. };
  771. }
  772. };
  773. };
  774. /**
  775. * Optimizes the visibility of the floating element by shifting it in order to
  776. * keep it in view when it will overflow the clipping boundary.
  777. * @see https://floating-ui.com/docs/shift
  778. */
  779. const shift = function (options) {
  780. if (options === void 0) {
  781. options = {};
  782. }
  783. return {
  784. name: 'shift',
  785. options,
  786. async fn(state) {
  787. const {
  788. x,
  789. y,
  790. placement
  791. } = state;
  792. const {
  793. mainAxis: checkMainAxis = true,
  794. crossAxis: checkCrossAxis = false,
  795. limiter = {
  796. fn: _ref => {
  797. let {
  798. x,
  799. y
  800. } = _ref;
  801. return {
  802. x,
  803. y
  804. };
  805. }
  806. },
  807. ...detectOverflowOptions
  808. } = evaluate(options, state);
  809. const coords = {
  810. x,
  811. y
  812. };
  813. const overflow = await detectOverflow(state, detectOverflowOptions);
  814. const crossAxis = getSideAxis(getSide(placement));
  815. const mainAxis = getOppositeAxis(crossAxis);
  816. let mainAxisCoord = coords[mainAxis];
  817. let crossAxisCoord = coords[crossAxis];
  818. if (checkMainAxis) {
  819. const minSide = mainAxis === 'y' ? 'top' : 'left';
  820. const maxSide = mainAxis === 'y' ? 'bottom' : 'right';
  821. const min = mainAxisCoord + overflow[minSide];
  822. const max = mainAxisCoord - overflow[maxSide];
  823. mainAxisCoord = clamp(min, mainAxisCoord, max);
  824. }
  825. if (checkCrossAxis) {
  826. const minSide = crossAxis === 'y' ? 'top' : 'left';
  827. const maxSide = crossAxis === 'y' ? 'bottom' : 'right';
  828. const min = crossAxisCoord + overflow[minSide];
  829. const max = crossAxisCoord - overflow[maxSide];
  830. crossAxisCoord = clamp(min, crossAxisCoord, max);
  831. }
  832. const limitedCoords = limiter.fn({
  833. ...state,
  834. [mainAxis]: mainAxisCoord,
  835. [crossAxis]: crossAxisCoord
  836. });
  837. return {
  838. ...limitedCoords,
  839. data: {
  840. x: limitedCoords.x - x,
  841. y: limitedCoords.y - y
  842. }
  843. };
  844. }
  845. };
  846. };
  847. /**
  848. * Built-in `limiter` that will stop `shift()` at a certain point.
  849. */
  850. const limitShift = function (options) {
  851. if (options === void 0) {
  852. options = {};
  853. }
  854. return {
  855. options,
  856. fn(state) {
  857. const {
  858. x,
  859. y,
  860. placement,
  861. rects,
  862. middlewareData
  863. } = state;
  864. const {
  865. offset = 0,
  866. mainAxis: checkMainAxis = true,
  867. crossAxis: checkCrossAxis = true
  868. } = evaluate(options, state);
  869. const coords = {
  870. x,
  871. y
  872. };
  873. const crossAxis = getSideAxis(placement);
  874. const mainAxis = getOppositeAxis(crossAxis);
  875. let mainAxisCoord = coords[mainAxis];
  876. let crossAxisCoord = coords[crossAxis];
  877. const rawOffset = evaluate(offset, state);
  878. const computedOffset = typeof rawOffset === 'number' ? {
  879. mainAxis: rawOffset,
  880. crossAxis: 0
  881. } : {
  882. mainAxis: 0,
  883. crossAxis: 0,
  884. ...rawOffset
  885. };
  886. if (checkMainAxis) {
  887. const len = mainAxis === 'y' ? 'height' : 'width';
  888. const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis;
  889. const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis;
  890. if (mainAxisCoord < limitMin) {
  891. mainAxisCoord = limitMin;
  892. } else if (mainAxisCoord > limitMax) {
  893. mainAxisCoord = limitMax;
  894. }
  895. }
  896. if (checkCrossAxis) {
  897. var _middlewareData$offse, _middlewareData$offse2;
  898. const len = mainAxis === 'y' ? 'width' : 'height';
  899. const isOriginSide = ['top', 'left'].includes(getSide(placement));
  900. const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis);
  901. const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0);
  902. if (crossAxisCoord < limitMin) {
  903. crossAxisCoord = limitMin;
  904. } else if (crossAxisCoord > limitMax) {
  905. crossAxisCoord = limitMax;
  906. }
  907. }
  908. return {
  909. [mainAxis]: mainAxisCoord,
  910. [crossAxis]: crossAxisCoord
  911. };
  912. }
  913. };
  914. };
  915. /**
  916. * Provides data that allows you to change the size of the floating element —
  917. * for instance, prevent it from overflowing the clipping boundary or match the
  918. * width of the reference element.
  919. * @see https://floating-ui.com/docs/size
  920. */
  921. const size = function (options) {
  922. if (options === void 0) {
  923. options = {};
  924. }
  925. return {
  926. name: 'size',
  927. options,
  928. async fn(state) {
  929. const {
  930. placement,
  931. rects,
  932. platform,
  933. elements
  934. } = state;
  935. const {
  936. apply = () => {},
  937. ...detectOverflowOptions
  938. } = evaluate(options, state);
  939. const overflow = await detectOverflow(state, detectOverflowOptions);
  940. const side = getSide(placement);
  941. const alignment = getAlignment(placement);
  942. const isYAxis = getSideAxis(placement) === 'y';
  943. const {
  944. width,
  945. height
  946. } = rects.floating;
  947. let heightSide;
  948. let widthSide;
  949. if (side === 'top' || side === 'bottom') {
  950. heightSide = side;
  951. widthSide = alignment === ((await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))) ? 'start' : 'end') ? 'left' : 'right';
  952. } else {
  953. widthSide = side;
  954. heightSide = alignment === 'end' ? 'top' : 'bottom';
  955. }
  956. const overflowAvailableHeight = height - overflow[heightSide];
  957. const overflowAvailableWidth = width - overflow[widthSide];
  958. const noShift = !state.middlewareData.shift;
  959. let availableHeight = overflowAvailableHeight;
  960. let availableWidth = overflowAvailableWidth;
  961. if (isYAxis) {
  962. const maximumClippingWidth = width - overflow.left - overflow.right;
  963. availableWidth = alignment || noShift ? min(overflowAvailableWidth, maximumClippingWidth) : maximumClippingWidth;
  964. } else {
  965. const maximumClippingHeight = height - overflow.top - overflow.bottom;
  966. availableHeight = alignment || noShift ? min(overflowAvailableHeight, maximumClippingHeight) : maximumClippingHeight;
  967. }
  968. if (noShift && !alignment) {
  969. const xMin = max(overflow.left, 0);
  970. const xMax = max(overflow.right, 0);
  971. const yMin = max(overflow.top, 0);
  972. const yMax = max(overflow.bottom, 0);
  973. if (isYAxis) {
  974. availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right));
  975. } else {
  976. availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom));
  977. }
  978. }
  979. await apply({
  980. ...state,
  981. availableWidth,
  982. availableHeight
  983. });
  984. const nextDimensions = await platform.getDimensions(elements.floating);
  985. if (width !== nextDimensions.width || height !== nextDimensions.height) {
  986. return {
  987. reset: {
  988. rects: true
  989. }
  990. };
  991. }
  992. return {};
  993. }
  994. };
  995. };
  996. export { arrow, autoPlacement, computePosition, detectOverflow, flip, hide, inline, limitShift, offset, shift, size };