UNPKG

@indevstudio/react-split-panel

Version:

React split-pane component for TypeScript and React 17+

590 lines (523 loc) 19.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = require('react'); var React__default = _interopDefault(React); var EventListener = require('@indevstudio/react-event-listener'); var EventListener__default = _interopDefault(EventListener); var styled = _interopDefault(require('@emotion/styled')); var isEqual = _interopDefault(require('lodash/isEqual')); var DEFAULT_PANE_SIZE = '1'; var DEFAULT_PANE_MIN_SIZE = '0'; var DEFAULT_PANE_MAX_SIZE = '100%'; function getUnit(size) { if (size.endsWith('px')) { return 'px'; } if (size.endsWith('%')) { return '%'; } return 'ratio'; } var convertSizeToCssValue = function convertSizeToCssValue(value, resizersSize) { if (getUnit(value) !== '%') { return value; } if (!resizersSize) { return value; } var idx = value.search('%'); var percent = +value.slice(0, idx) / 100; if (percent === 0) { return value; } return "calc(" + value + " - " + resizersSize + "px*" + percent + ")"; }; var convertToUnit = function convertToUnit(size, unit, containerSize) { switch (unit) { case '%': return (size / containerSize * 100).toFixed(2) + "%"; case 'px': return size.toFixed(2) + "px"; case 'ratio': return (size * 100).toFixed(0); } throw new Error('unit out of bound!'); }; var removeNullChildren = function removeNullChildren(children) { return React__default.Children.toArray(children).filter(function (c) { return c; }); }; var getPanePropSize = function getPanePropSize(children) { return removeNullChildren(children).map(function (child) { var _child$props$size; var value = (_child$props$size = child['props']['size']) != null ? _child$props$size : child.props['initialSize']; return value != null ? value : DEFAULT_PANE_SIZE; }); }; var getPanePropMinMaxSize = function getPanePropMinMaxSize(children, key) { return removeNullChildren(children).map(function (child) { var value = child.props[key]; if (value === undefined) { return key === 'maxSize' ? DEFAULT_PANE_MAX_SIZE : DEFAULT_PANE_MIN_SIZE; } return value; }); }; var getResizersSize = function getResizersSize(children, resizerSize) { if (!children) { return 0; } return (React__default.Children.count(children) - 1) * resizerSize; }; var getPaneDimensions = function getPaneDimensions(paneElements) { return paneElements.filter(function (el) { return el; }).map(function (el) { return el.getBoundingClientRect(); }); }; var getDimensionsSnapshot = function getDimensionsSnapshot(children, resizerSize, split, paneElements, splitPane) { var paneDimensions = getPaneDimensions(paneElements); var splitPaneDimensions = splitPane.getBoundingClientRect(); var minSizes = getPanePropMinMaxSize(children, 'minSize'); var maxSizes = getPanePropMinMaxSize(children, 'maxSize'); var notNullChildren = removeNullChildren(children); var resizersSize = getResizersSize(notNullChildren, resizerSize); var splitPaneSizePx = split === 'vertical' ? splitPaneDimensions.width - resizersSize : splitPaneDimensions.height - resizersSize; var minSizesPx = minSizes.map(function (s) { return convert(s, splitPaneSizePx); }); var maxSizesPx = maxSizes.map(function (s) { return convert(s, splitPaneSizePx); }); var sizesPx = paneDimensions.map(function (d) { return split === 'vertical' ? d.width : d.height; }); return { resizersSize: resizersSize, paneDimensions: paneDimensions, splitPaneSizePx: splitPaneSizePx, minSizesPx: minSizesPx, maxSizesPx: maxSizesPx, sizesPx: sizesPx }; }; var toPx = function toPx(value, unit, size) { switch (unit) { case '%': { return +(size * value / 100).toFixed(2); } default: { return +value; } } }; var convert = function convert(str, size) { var tokens = str.match(/([0-9]+)([px|%]*)/); if (!tokens) { return 0; } var value = tokens[1]; var unit = tokens[2]; return toPx(+value, unit, size); }; var calculateSizes = function calculateSizes(props) { var split = props.split, resizerIndex = props.resizerIndex, dimensionsSnapshot = props.dimensionsSnapshot, startClientX = props.startClientX, startClientY = props.startClientY, clientX = props.clientX, clientY = props.clientY, sizes = props.sizes; var sizesPx = dimensionsSnapshot.sizesPx, minSizesPx = dimensionsSnapshot.minSizesPx, maxSizesPx = dimensionsSnapshot.maxSizesPx, splitPaneSizePx = dimensionsSnapshot.splitPaneSizePx, paneDimensions = dimensionsSnapshot.paneDimensions; var sizeDim = split === 'vertical' ? 'width' : 'height'; var primary = paneDimensions[resizerIndex]; var secondary = paneDimensions[resizerIndex + 1]; var maxSize = primary[sizeDim] + secondary[sizeDim]; var primaryMinSizePx = minSizesPx[resizerIndex]; var secondaryMinSizePx = minSizesPx[resizerIndex + 1]; var primaryMaxSizePx = Math.min(maxSizesPx[resizerIndex], maxSize); var secondaryMaxSizePx = Math.min(maxSizesPx[resizerIndex + 1], maxSize); var moveOffset = split === 'vertical' ? startClientX - clientX : startClientY - clientY; var primarySizePx = primary[sizeDim] - moveOffset; var secondarySizePx = secondary[sizeDim] + moveOffset; var primaryHasReachedLimit = false; var secondaryHasReachedLimit = false; if (primarySizePx < primaryMinSizePx) { primarySizePx = primaryMinSizePx; primaryHasReachedLimit = true; } else if (primarySizePx > primaryMaxSizePx) { primarySizePx = primaryMaxSizePx; primaryHasReachedLimit = true; } if (secondarySizePx < secondaryMinSizePx) { secondarySizePx = secondaryMinSizePx; secondaryHasReachedLimit = true; } else if (secondarySizePx > secondaryMaxSizePx) { secondarySizePx = secondaryMaxSizePx; secondaryHasReachedLimit = true; } if (primaryHasReachedLimit) { secondarySizePx = primary[sizeDim] + secondary[sizeDim] - primarySizePx; } else if (secondaryHasReachedLimit) { primarySizePx = primary[sizeDim] + secondary[sizeDim] - secondarySizePx; } sizesPx[resizerIndex] = primarySizePx; sizesPx[resizerIndex + 1] = secondarySizePx; var newSizes = sizes.concat(); var updateRatio; [primarySizePx, secondarySizePx].forEach(function (paneSize, idx) { var unit = getUnit(sizes[resizerIndex + idx]); if (unit !== 'ratio') { newSizes[resizerIndex + idx] = convertToUnit(paneSize, unit, splitPaneSizePx); } else { updateRatio = true; } }); if (updateRatio) { var ratioCount = 0; var lastRatioIdx = 0; newSizes = newSizes.map(function (size, idx) { if (getUnit(size) === 'ratio') { ratioCount++; lastRatioIdx = idx; return convertToUnit(sizesPx[idx], 'ratio', 0); } return size; }); if (ratioCount === 1) { newSizes[lastRatioIdx] = '1'; } } return newSizes; }; var paneStyle = function paneStyle(props) { var _ref; var __internal = props.__internal, initialSize = props.initialSize, minSize = props.minSize, maxSize = props.maxSize; if (!__internal) { throw new Error("__internal is null"); } var split = __internal.split, size = __internal.size, resizersSize = __internal.resizersSize; var value = (_ref = size != null ? size : initialSize) != null ? _ref : "1"; var vertical = split === 'vertical'; var styleProp = { minSize: vertical ? 'minWidth' : 'minHeight', maxSize: vertical ? 'maxWidth' : 'maxHeight', size: vertical ? 'width' : 'height' }; var style = { display: 'flex', outline: 'none' }; style[styleProp.minSize] = minSize && convertSizeToCssValue(minSize, resizersSize); style[styleProp.maxSize] = maxSize && convertSizeToCssValue(maxSize, resizersSize); switch (getUnit(value)) { case 'ratio': style.flex = value; break; case '%': case 'px': style.flexGrow = 0; style[styleProp.size] = convertSizeToCssValue(value, resizersSize); break; } return style; }; var Pane = /*#__PURE__*/React.forwardRef(function (props, ref) { var children = props.children, className = props.className; var prefixedStyle = paneStyle(props); return React__default.createElement("div", { className: className, style: prefixedStyle, ref: ref }, children); }); function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _taggedTemplateLiteralLoose(strings, raw) { if (!raw) { raw = strings.slice(0); } strings.raw = raw; return strings; } var _templateObject, _templateObject2, _templateObject3; var Wrapper = /*#__PURE__*/styled.div(_templateObject || (_templateObject = /*#__PURE__*/_taggedTemplateLiteralLoose(["\n background: #000;\n opacity: 0.2;\n z-index: 1;\n box-sizing: border-box;\n background-clip: padding-box;\n\n :hover {\n transition: all 2s ease;\n }\n"]))); var HorizontalWrapper = /*#__PURE__*/styled(Wrapper)(_templateObject2 || (_templateObject2 = /*#__PURE__*/_taggedTemplateLiteralLoose(["\n height: 11px;\n margin: -5px 0;\n border-top: 5px solid rgba(255, 255, 255, 0);\n border-bottom: 5px solid rgba(255, 255, 255, 0);\n cursor: row-resize;\n width: 100%;\n\n :hover {\n border-top: 5px solid rgba(0, 0, 0, 0.5);\n border-bottom: 5px solid rgba(0, 0, 0, 0.5);\n }\n\n .disabled {\n cursor: not-allowed;\n }\n .disabled:hover {\n border-color: transparent;\n }\n"]))); var VerticalWrapper = /*#__PURE__*/styled(Wrapper)(_templateObject3 || (_templateObject3 = /*#__PURE__*/_taggedTemplateLiteralLoose(["\n width: 11px;\n margin: 0 -5px;\n border-left: 5px solid rgba(255, 255, 255, 0);\n border-right: 5px solid rgba(255, 255, 255, 0);\n cursor: col-resize;\n\n :hover {\n border-left: 5px solid rgba(0, 0, 0, 0.5);\n border-right: 5px solid rgba(0, 0, 0, 0.5);\n }\n .disabled {\n cursor: not-allowed;\n }\n .disabled:hover {\n border-color: transparent;\n }\n"]))); var Resizer = function Resizer(props) { var index = props.index, split = props.split, onMouseDown = props.onMouseDown, onTouchStart = props.onTouchStart, style = props.style, className = props.className; var onMouseDownHandler = React.useCallback(function (event) { if (!onMouseDown) { return; } event.preventDefault(); onMouseDown(event, index); }, []); var onTouchEndHandler = React.useCallback(function (event) { event.preventDefault(); }, []); var onTouchStartHandler = React.useCallback(function (event) { event.preventDefault(); onTouchStart(event, index); }, []); var wrapper = { 'data-attribute': split, 'data-type': 'Resizer', onMouseDown: onMouseDownHandler, onTouchEnd: onTouchEndHandler, onTouchStart: onTouchStartHandler, className: className, style: style }; return split === 'vertical' ? React__default.createElement(VerticalWrapper, Object.assign({}, wrapper)) : React__default.createElement(HorizontalWrapper, Object.assign({}, wrapper)); }; var SplitPanelChildren = function SplitPanelChildren(props) { var children = props.children, sizes = props.sizes, resizerSize = props.resizerSize, split = props.split, paneElementsRef = props.paneElementsRef, onResizerMouseDown = props.onResizerMouseDown, onResizerTouchStart = props.onResizerTouchStart, resizerStyle = props.resizerStyle, resizerClassName = props.resizerClassName; var notNullChildren = React.useMemo(function () { return removeNullChildren(children); }, [children]); var paneStyleProps = React.useMemo(function () { var resizersSize = getResizersSize(notNullChildren, resizerSize); return notNullChildren.map(function (child, idx) { return _extends({}, child.props, { key: "Pane-" + idx, 'data-type': 'Pane', ref: function ref(el) { return paneElementsRef.current[idx] = el; }, __internal: { split: split, resizersSize: resizersSize, size: sizes[idx] } }); }); }, [split, resizerSize, sizes, paneElementsRef, notNullChildren]); var resizerProps = { split: split, onMouseDown: onResizerMouseDown, onTouchStart: onResizerTouchStart, style: resizerStyle, className: resizerClassName }; return React__default.createElement(React__default.Fragment, null, paneStyleProps.map(function (pane, idx) { var child = notNullChildren[idx]; return React__default.createElement(React__default.Fragment, { key: "idx" }, idx !== 0 && React__default.createElement(Resizer, Object.assign({ index: idx - 1, key: "Resizer-" + (idx - 1) }, resizerProps)), React__default.createElement(React__default.Fragment, { key: "Pane-" + idx }, child.type === Pane ? React.cloneElement(child, pane) : React__default.createElement(Pane, Object.assign({}, pane), child))); })); }; var ColumnStyle = /*#__PURE__*/styled.div({ display: 'flex', height: '100%', flexDirection: 'column', flex: 1, outline: 'none', overflow: 'hidden', userSelect: 'text' }); var RowStyle = /*#__PURE__*/styled.div({ display: 'flex', height: '100%', flexDirection: 'row', flex: 1, outline: 'none', overflow: 'hidden', userSelect: 'text' }); var SplitPanel = function SplitPanel(props) { var children = props.children, onResizeEnd = props.onResizeEnd, disallowResize = props.disallowResize, onResizeStart = props.onResizeStart, split = props.split, resizerSize = props.resizerSize, onChange = props.onChange, className = props.className, style = props.style, resizerClassName = props.resizerClassName, resizerStyle = props.resizerStyle; var paneElementsRef = React.useRef([]); React.useEffect(function () { paneElementsRef.current = paneElementsRef.current.slice(0, React__default.Children.count(children)); }, [children]); var splitPaneRef = React.useRef(null); var _useState = React.useState([]), sizes = _useState[0], setSizes = _useState[1]; var _useState2 = React.useState(false), enableListen = _useState2[0], setEnableListen = _useState2[1]; var _useState3 = React.useState(0), resizerIndex = _useState3[0], setResizerIndex = _useState3[1]; var _useState4 = React.useState(), dimensionsSnapshot = _useState4[0], setDimensionsSnapshot = _useState4[1]; var _useState5 = React.useState(0), startClientX = _useState5[0], setStartClientX = _useState5[1]; var _useState6 = React.useState(0), startClientY = _useState6[0], setStartClientY = _useState6[1]; var onDown = React.useCallback(function (resizerIndex, clientX, clientY) { if (disallowResize || !splitPaneRef.current) { return; } setResizerIndex(resizerIndex); var snapshot = getDimensionsSnapshot(children, resizerSize, split, paneElementsRef.current, splitPaneRef.current); setDimensionsSnapshot(snapshot); setStartClientX(clientX); setStartClientY(clientY); setEnableListen(true); if (onResizeStart) { onResizeStart(); } }, [children, resizerSize, split, paneElementsRef.current, splitPaneRef.current, disallowResize, onResizeStart, setDimensionsSnapshot, setStartClientX, setStartClientY, setEnableListen]); var onMove = React.useCallback(function (clientX, clientY) { if (!dimensionsSnapshot) { return; } var newSizes = calculateSizes({ clientX: clientX, clientY: clientY, startClientX: startClientX, dimensionsSnapshot: dimensionsSnapshot, resizerIndex: resizerIndex, sizes: sizes, split: split, startClientY: startClientY }); onChange && onChange(newSizes); setSizes(newSizes); }, [split, onChange, startClientX, dimensionsSnapshot, resizerIndex, sizes, split, startClientY, setSizes]); var onMouseUp = React.useCallback(function (event) { event.preventDefault(); setEnableListen(false); if (onResizeEnd) { onResizeEnd(sizes); } }, [sizes, setEnableListen, onResizeEnd]); var onMouseMove = React.useCallback(function (event) { event.preventDefault(); onMove(event.clientX, event.clientY); }, [onMove]); var onTouchMove = React.useCallback(function (event) { event.preventDefault(); var _event$touches$ = event.touches[0], clientX = _event$touches$.clientX, clientY = _event$touches$.clientY; onMove(clientX, clientY); }, [onMove]); var onResizerMouseDown = React.useCallback(function (event, resizerIndex) { if (event.button !== 0) { return; } event.preventDefault(); onDown(resizerIndex, event.clientX, event.clientY); }, [onDown]); var onResizerTouchStart = React.useCallback(function (event, resizerIndex) { event.preventDefault(); var _event$touches$2 = event.touches[0], clientX = _event$touches$2.clientX, clientY = _event$touches$2.clientY; onDown(resizerIndex, clientX, clientY); }, [onDown]); React.useEffect(function () { var newSizes = getPanePropSize(children); if (!isEqual(sizes, newSizes)) { setSizes(newSizes); } }, [setSizes, getPanePropSize, children]); var splitPaneChildren = { onResizerMouseDown: onResizerMouseDown, onResizerTouchStart: onResizerTouchStart, paneElementsRef: paneElementsRef, resizerSize: resizerSize, sizes: sizes, split: split, resizerClassName: resizerClassName, resizerStyle: resizerStyle }; var StyleComponent = split === 'vertical' ? RowStyle : ColumnStyle; return React__default.createElement(React__default.Fragment, null, enableListen && React__default.createElement(EventListener__default, { target: document, onMouseUp: EventListener.withOptions(onMouseUp, { passive: false }), onMouseMove: EventListener.withOptions(onMouseMove, { passive: false }), onTouchMove: EventListener.withOptions(onTouchMove, { passive: false }), onTouchEnd: EventListener.withOptions(onMouseUp, { passive: false }), onTouchCancel: EventListener.withOptions(onMouseUp, { passive: false }) }), React__default.createElement(StyleComponent, { className: className, style: style, "data-type": "SplitPane", "data-split": split, ref: splitPaneRef }, React__default.createElement(SplitPanelChildren, Object.assign({}, splitPaneChildren), children))); }; var useSplitPanel = function useSplitPanel(props) { return _extends({ resizerSize: 1, split: 'vertical' }, props); }; exports.Pane = Pane; exports.SplitPanel = SplitPanel; exports.useSplitPanel = useSplitPanel; //# sourceMappingURL=react-split-panel.cjs.development.js.map