SplitBar.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. "use client";
  2. import React, { useState } from 'react';
  3. import DownOutlined from "@ant-design/icons/es/icons/DownOutlined";
  4. import LeftOutlined from "@ant-design/icons/es/icons/LeftOutlined";
  5. import RightOutlined from "@ant-design/icons/es/icons/RightOutlined";
  6. import UpOutlined from "@ant-design/icons/es/icons/UpOutlined";
  7. import classNames from 'classnames';
  8. import useEvent from "rc-util/es/hooks/useEvent";
  9. import useLayoutEffect from "rc-util/es/hooks/useLayoutEffect";
  10. function getValidNumber(num) {
  11. return typeof num === 'number' && !Number.isNaN(num) && Number.isFinite(num) ? Math.round(num) : 0;
  12. }
  13. const SplitBar = props => {
  14. const {
  15. prefixCls,
  16. vertical,
  17. index,
  18. active,
  19. ariaNow,
  20. ariaMin,
  21. ariaMax,
  22. resizable,
  23. startCollapsible,
  24. endCollapsible,
  25. onOffsetStart,
  26. onOffsetUpdate,
  27. onOffsetEnd,
  28. onCollapse,
  29. lazy,
  30. containerSize,
  31. showStartCollapsibleIcon,
  32. showEndCollapsibleIcon
  33. } = props;
  34. const splitBarPrefixCls = `${prefixCls}-bar`;
  35. // ======================== Resize ========================
  36. const [startPos, setStartPos] = useState(null);
  37. const [constrainedOffset, setConstrainedOffset] = useState(0);
  38. const constrainedOffsetX = vertical ? 0 : constrainedOffset;
  39. const constrainedOffsetY = vertical ? constrainedOffset : 0;
  40. const onMouseDown = e => {
  41. if (resizable && e.currentTarget) {
  42. setStartPos([e.pageX, e.pageY]);
  43. onOffsetStart(index);
  44. }
  45. };
  46. const onTouchStart = e => {
  47. if (resizable && e.touches.length === 1) {
  48. const touch = e.touches[0];
  49. setStartPos([touch.pageX, touch.pageY]);
  50. onOffsetStart(index);
  51. }
  52. };
  53. // Updated constraint calculation
  54. const getConstrainedOffset = rawOffset => {
  55. const currentPos = containerSize * ariaNow / 100;
  56. const newPos = currentPos + rawOffset;
  57. // Calculate available space
  58. const minAllowed = Math.max(0, containerSize * ariaMin / 100);
  59. const maxAllowed = Math.min(containerSize, containerSize * ariaMax / 100);
  60. // Constrain new position within bounds
  61. const clampedPos = Math.max(minAllowed, Math.min(maxAllowed, newPos));
  62. return clampedPos - currentPos;
  63. };
  64. const handleLazyMove = useEvent((offsetX, offsetY) => {
  65. const constrainedOffsetValue = getConstrainedOffset(vertical ? offsetY : offsetX);
  66. setConstrainedOffset(constrainedOffsetValue);
  67. });
  68. const handleLazyEnd = useEvent(() => {
  69. onOffsetUpdate(index, constrainedOffsetX, constrainedOffsetY, true);
  70. setConstrainedOffset(0);
  71. onOffsetEnd(true);
  72. });
  73. const getVisibilityClass = mode => {
  74. switch (mode) {
  75. case true:
  76. return `${splitBarPrefixCls}-collapse-bar-always-visible`;
  77. case false:
  78. return `${splitBarPrefixCls}-collapse-bar-always-hidden`;
  79. case 'auto':
  80. return `${splitBarPrefixCls}-collapse-bar-hover-only`;
  81. }
  82. };
  83. useLayoutEffect(() => {
  84. if (!startPos) {
  85. return;
  86. }
  87. const onMouseMove = e => {
  88. const {
  89. pageX,
  90. pageY
  91. } = e;
  92. const offsetX = pageX - startPos[0];
  93. const offsetY = pageY - startPos[1];
  94. if (lazy) {
  95. handleLazyMove(offsetX, offsetY);
  96. } else {
  97. onOffsetUpdate(index, offsetX, offsetY);
  98. }
  99. };
  100. const onMouseUp = () => {
  101. if (lazy) {
  102. handleLazyEnd();
  103. } else {
  104. onOffsetEnd();
  105. }
  106. setStartPos(null);
  107. };
  108. const handleTouchMove = e => {
  109. if (e.touches.length === 1) {
  110. const touch = e.touches[0];
  111. const offsetX = touch.pageX - startPos[0];
  112. const offsetY = touch.pageY - startPos[1];
  113. if (lazy) {
  114. handleLazyMove(offsetX, offsetY);
  115. } else {
  116. onOffsetUpdate(index, offsetX, offsetY);
  117. }
  118. }
  119. };
  120. const handleTouchEnd = () => {
  121. if (lazy) {
  122. handleLazyEnd();
  123. } else {
  124. onOffsetEnd();
  125. }
  126. setStartPos(null);
  127. };
  128. const eventHandlerMap = {
  129. mousemove: onMouseMove,
  130. mouseup: onMouseUp,
  131. touchmove: handleTouchMove,
  132. touchend: handleTouchEnd
  133. };
  134. for (const [event, handler] of Object.entries(eventHandlerMap)) {
  135. // eslint-disable-next-line react-web-api/no-leaked-event-listener
  136. window.addEventListener(event, handler);
  137. }
  138. return () => {
  139. for (const [event, handler] of Object.entries(eventHandlerMap)) {
  140. window.removeEventListener(event, handler);
  141. }
  142. };
  143. }, [startPos, index, lazy]);
  144. const transformStyle = {
  145. [`--${splitBarPrefixCls}-preview-offset`]: `${constrainedOffset}px`
  146. };
  147. // ======================== Render ========================
  148. const StartIcon = vertical ? UpOutlined : LeftOutlined;
  149. const EndIcon = vertical ? DownOutlined : RightOutlined;
  150. return /*#__PURE__*/React.createElement("div", {
  151. className: splitBarPrefixCls,
  152. role: "separator",
  153. "aria-valuenow": getValidNumber(ariaNow),
  154. "aria-valuemin": getValidNumber(ariaMin),
  155. "aria-valuemax": getValidNumber(ariaMax)
  156. }, lazy && (/*#__PURE__*/React.createElement("div", {
  157. className: classNames(`${splitBarPrefixCls}-preview`, {
  158. [`${splitBarPrefixCls}-preview-active`]: !!constrainedOffset
  159. }),
  160. style: transformStyle
  161. })), /*#__PURE__*/React.createElement("div", {
  162. className: classNames(`${splitBarPrefixCls}-dragger`, {
  163. [`${splitBarPrefixCls}-dragger-disabled`]: !resizable,
  164. [`${splitBarPrefixCls}-dragger-active`]: active
  165. }),
  166. onMouseDown: onMouseDown,
  167. onTouchStart: onTouchStart
  168. }), startCollapsible && (/*#__PURE__*/React.createElement("div", {
  169. className: classNames(`${splitBarPrefixCls}-collapse-bar`, `${splitBarPrefixCls}-collapse-bar-start`, getVisibilityClass(showStartCollapsibleIcon)),
  170. onClick: () => onCollapse(index, 'start')
  171. }, /*#__PURE__*/React.createElement(StartIcon, {
  172. className: classNames(`${splitBarPrefixCls}-collapse-icon`, `${splitBarPrefixCls}-collapse-start`)
  173. }))), endCollapsible && (/*#__PURE__*/React.createElement("div", {
  174. className: classNames(`${splitBarPrefixCls}-collapse-bar`, `${splitBarPrefixCls}-collapse-bar-end`, getVisibilityClass(showEndCollapsibleIcon)),
  175. onClick: () => onCollapse(index, 'end')
  176. }, /*#__PURE__*/React.createElement(EndIcon, {
  177. className: classNames(`${splitBarPrefixCls}-collapse-icon`, `${splitBarPrefixCls}-collapse-end`)
  178. }))));
  179. };
  180. export default SplitBar;