/* 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(
"resource://devtools/client/shared/vendor/react.js");
const PropTypes = require(
"resource://devtools/client/shared/vendor/react-prop-types.js");
const dom = require(
"resource://devtools/client/shared/vendor/react-dom-factories.js");
const Draggable = createFactory(
require(
"resource://devtools/client/shared/components/splitter/Draggable.js")
);
/**
* 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.oneOfType([PropTypes.number, 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,
// Left/top panel collapse state.
startPanelCollapsed: PropTypes.bool,
// 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,
// Right/bottom panel collapse state.
endPanelCollapsed: PropTypes.bool,
// 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,
// Optional callback when splitbox resize stops
onResizeEnd: PropTypes.func,
// Retrieve DOM reference to the start panel element
onSelectContainerElement: PropTypes.any,
};
}
static get defaultProps() {
return {
splitterSize: 5,
vert:
true,
endPanelControl:
false,
};
}
static getDerivedStateFromProps(props, state) {
if (
props.endPanelControl === state.prevEndPanelControl &&
props.splitterSize === state.prevSplitterSize &&
props.vert === state.prevVert
) {
return null;
}
const newState = {};
if (props.endPanelControl !== state.prevEndPanelControl) {
newState.endPanelControl = props.endPanelControl;
newState.prevEndPanelControl = props.endPanelControl;
}
if (props.splitterSize !== state.prevSplitterSize) {
newState.splitterSize = props.splitterSize;
newState.prevSplitterSize = props.splitterSize;
}
if (props.vert !== state.prevVert) {
newState.vert = props.vert;
newState.prevVert = props.vert;
}
return newState;
}
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,
// The state for above 3 properties are derived from props, but also managed by the component itself.
// SplitBox manages it's own state but sometimes the parent will pass in new props which will
// override the current state of the component. So we need track the prev value of these props so that
// compare them to the props change and derive new state whenever these 3 props change.
prevEndPanelControl: props.endPanelControl,
prevVert: props.vert,
prevSplitterSize: 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);
}
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,
});
}
onStopMove() {
const doc =
this.splitBox.ownerDocument;
doc.documentElement.style.cursor =
this.state.defaultCursor;
this.splitBox.classList.remove(
"dragging");
if (
this.props.onResizeEnd) {
this.props.onResizeEnd(
this.state.vert ?
this.state.width :
this.state.height
);
}
}
/**
* 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,
startPanelCollapsed,
endPanel,
endPanelCollapsed,
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 && !startPanelCollapsed
? 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 && !endPanelCollapsed
? dom.div(
{
className: endPanelControl ?
"controlled" :
"uncontrolled",
style: rightPanelStyle,
role:
"presentation",
ref: div => {
this.endPanelContainer = div;
},
},
endPanel
)
:
null
);
}
}
module.exports = SplitBox;