diff options
Diffstat (limited to 'devtools/client/shared/components/splitter/SplitBox.js')
-rw-r--r-- | devtools/client/shared/components/splitter/SplitBox.js | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/devtools/client/shared/components/splitter/SplitBox.js b/devtools/client/shared/components/splitter/SplitBox.js new file mode 100644 index 0000000000..a8bb3279aa --- /dev/null +++ b/devtools/client/shared/components/splitter/SplitBox.js @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const dom = require("devtools/client/shared/vendor/react-dom-factories"); + +const Draggable = createFactory( + require("devtools/client/shared/components/splitter/Draggable") +); + +/** + * This component represents a Splitter. The splitter supports vertical + * as well as horizontal mode. + */ +class SplitBox extends Component { + static get propTypes() { + return { + // Custom class name. You can use more names separated by a space. + className: PropTypes.string, + // Initial size of controlled panel. + initialSize: PropTypes.string, + // Initial width of controlled panel. + initialWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + // Initial height of controlled panel. + initialHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + // Left/top panel + startPanel: PropTypes.any, + // Min panel size. + minSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + // Max panel size. + maxSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + // Right/bottom panel + endPanel: PropTypes.any, + // True if the right/bottom panel should be controlled. + endPanelControl: PropTypes.bool, + // Size of the splitter handle bar. + splitterSize: PropTypes.number, + // True if the splitter bar is vertical (default is vertical). + vert: PropTypes.bool, + // Style object. + style: PropTypes.object, + // Call when controlled panel was resized. + onControlledPanelResized: PropTypes.func, + // Retrieve DOM reference to the start panel element + onSelectContainerElement: PropTypes.any, + }; + } + + static get defaultProps() { + return { + splitterSize: 5, + vert: true, + endPanelControl: false, + }; + } + + constructor(props) { + super(props); + + /** + * The state stores whether or not the end panel should be controlled, the current + * orientation (vertical or horizontal), the splitter size, and the current size + * (width/height). All these values can change during the component's life time. + */ + this.state = { + // True if the right/bottom panel should be controlled. + endPanelControl: props.endPanelControl, + // True if the splitter bar is vertical (default is vertical). + vert: props.vert, + // Size of the splitter handle bar. + splitterSize: props.splitterSize, + // Width of controlled panel. + width: props.initialWidth || props.initialSize, + // Height of controlled panel. + height: props.initialHeight || props.initialSize, + }; + + this.onStartMove = this.onStartMove.bind(this); + this.onStopMove = this.onStopMove.bind(this); + this.onMove = this.onMove.bind(this); + } + + componentWillReceiveProps(nextProps) { + const { endPanelControl, splitterSize, vert } = nextProps; + + if (endPanelControl != this.props.endPanelControl) { + this.setState({ endPanelControl }); + } + + if (splitterSize != this.props.splitterSize) { + this.setState({ splitterSize }); + } + + if (vert !== this.props.vert) { + this.setState({ vert }); + } + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + nextState.width != this.state.width || + nextState.endPanelControl != this.props.endPanelControl || + nextState.height != this.state.height || + nextState.vert != this.state.vert || + nextState.splitterSize != this.state.splitterSize || + nextProps.startPanel != this.props.startPanel || + nextProps.endPanel != this.props.endPanel || + nextProps.minSize != this.props.minSize || + nextProps.maxSize != this.props.maxSize + ); + } + + componentDidUpdate(prevProps, prevState) { + if ( + this.props.onControlledPanelResized && + (prevState.width !== this.state.width || + prevState.height !== this.state.height) + ) { + this.props.onControlledPanelResized(this.state.width, this.state.height); + } + } + + // Dragging Events + + /** + * Set 'resizing' cursor on entire document during splitter dragging. + * This avoids cursor-flickering that happens when the mouse leaves + * the splitter bar area (happens frequently). + */ + onStartMove() { + const doc = this.splitBox.ownerDocument; + const defaultCursor = doc.documentElement.style.cursor; + doc.documentElement.style.cursor = this.state.vert + ? "ew-resize" + : "ns-resize"; + + this.splitBox.classList.add("dragging"); + + this.setState({ + defaultCursor: defaultCursor, + }); + } + + onStopMove() { + const doc = this.splitBox.ownerDocument; + doc.documentElement.style.cursor = this.state.defaultCursor; + + this.splitBox.classList.remove("dragging"); + } + + /** + * Adjust size of the controlled panel. Depending on the current + * orientation we either remember the width or height of + * the splitter box. + */ + onMove(x, y) { + const nodeBounds = this.splitBox.getBoundingClientRect(); + + let size; + let { endPanelControl, vert } = this.state; + + if (vert) { + // Use the document owning the SplitBox to detect rtl. The global document might be + // the one bound to the toolbox shared BrowserRequire, which is irrelevant here. + const doc = this.splitBox.ownerDocument; + + // Switch the control flag in case of RTL. Note that RTL + // has impact on vertical splitter only. + if (doc.dir === "rtl") { + endPanelControl = !endPanelControl; + } + + size = endPanelControl + ? nodeBounds.left + nodeBounds.width - x + : x - nodeBounds.left; + + this.setState({ + width: this.getConstrainedSizeInPx(size, nodeBounds.width), + }); + } else { + size = endPanelControl + ? nodeBounds.top + nodeBounds.height - y + : y - nodeBounds.top; + + this.setState({ + height: this.getConstrainedSizeInPx(size, nodeBounds.height), + }); + } + } + + /** + * Calculates the constrained size taking into account the minimum width or + * height passed via this.props.minSize. + * + * @param {Number} requestedSize + * The requested size + * @param {Number} splitBoxWidthOrHeight + * The width or height of the splitBox + * + * @return {Number} + * The constrained size + */ + getConstrainedSizeInPx(requestedSize, splitBoxWidthOrHeight) { + let minSize = this.props.minSize + ""; + + if (minSize.endsWith("%")) { + minSize = (parseFloat(minSize) / 100) * splitBoxWidthOrHeight; + } else if (minSize.endsWith("px")) { + minSize = parseFloat(minSize); + } + return Math.max(requestedSize, minSize); + } + + // Rendering + + // eslint-disable-next-line complexity + render() { + const { endPanelControl, splitterSize, vert } = this.state; + const { + startPanel, + endPanel, + minSize, + maxSize, + onSelectContainerElement, + } = this.props; + + const style = Object.assign( + { + // Set the size of the controlled panel (height or width depending on the + // current state). This can be used to help with styling of dependent + // panels. + "--split-box-controlled-panel-size": `${ + vert ? this.state.width : this.state.height + }`, + }, + this.props.style + ); + + // Calculate class names list. + let classNames = ["split-box"]; + classNames.push(vert ? "vert" : "horz"); + if (this.props.className) { + classNames = classNames.concat(this.props.className.split(" ")); + } + + let leftPanelStyle; + let rightPanelStyle; + + // Set proper size for panels depending on the current state. + if (vert) { + leftPanelStyle = { + maxWidth: endPanelControl ? null : maxSize, + minWidth: endPanelControl ? null : minSize, + width: endPanelControl ? null : this.state.width, + }; + rightPanelStyle = { + maxWidth: endPanelControl ? maxSize : null, + minWidth: endPanelControl ? minSize : null, + width: endPanelControl ? this.state.width : null, + }; + } else { + leftPanelStyle = { + maxHeight: endPanelControl ? null : maxSize, + minHeight: endPanelControl ? null : minSize, + height: endPanelControl ? null : this.state.height, + }; + rightPanelStyle = { + maxHeight: endPanelControl ? maxSize : null, + minHeight: endPanelControl ? minSize : null, + height: endPanelControl ? this.state.height : null, + }; + } + + // Calculate splitter size + const splitterStyle = { + flex: "0 0 " + splitterSize + "px", + }; + + return dom.div( + { + className: classNames.join(" "), + ref: div => { + this.splitBox = div; + }, + style, + }, + startPanel + ? dom.div( + { + className: endPanelControl ? "uncontrolled" : "controlled", + style: leftPanelStyle, + role: "presentation", + ref: div => { + this.startPanelContainer = div; + if (onSelectContainerElement) { + onSelectContainerElement(div); + } + }, + }, + startPanel + ) + : null, + splitterSize > 0 + ? Draggable({ + className: "splitter", + style: splitterStyle, + onStart: this.onStartMove, + onStop: this.onStopMove, + onMove: this.onMove, + }) + : null, + endPanel + ? dom.div( + { + className: endPanelControl ? "controlled" : "uncontrolled", + style: rightPanelStyle, + role: "presentation", + ref: div => { + this.endPanelContainer = div; + }, + }, + endPanel + ) + : null + ); + } +} + +module.exports = SplitBox; |