SplitBox.js (11657B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 Component, 9 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 14 const Draggable = createFactory( 15 require("resource://devtools/client/shared/components/splitter/Draggable.js") 16 ); 17 18 /** 19 * This component represents a Splitter. The splitter supports vertical 20 * as well as horizontal mode. 21 */ 22 class SplitBox extends Component { 23 static get propTypes() { 24 return { 25 // Custom class name. You can use more names separated by a space. 26 className: PropTypes.string, 27 // Initial size of controlled panel. 28 initialSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 29 // Initial width of controlled panel. 30 initialWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 31 // Initial height of controlled panel. 32 initialHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 33 // Left/top panel 34 startPanel: PropTypes.any, 35 // Left/top panel collapse state. 36 startPanelCollapsed: PropTypes.bool, 37 // Min panel size. 38 minSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 39 // Max panel size. 40 maxSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 41 // Right/bottom panel 42 endPanel: PropTypes.any, 43 // Right/bottom panel collapse state. 44 endPanelCollapsed: PropTypes.bool, 45 // True if the right/bottom panel should be controlled. 46 endPanelControl: PropTypes.bool, 47 // Size of the splitter handle bar. 48 splitterSize: PropTypes.number, 49 // True if the splitter bar is vertical (default is vertical). 50 vert: PropTypes.bool, 51 // Style object. 52 style: PropTypes.object, 53 // Call when controlled panel was resized. 54 onControlledPanelResized: PropTypes.func, 55 // Optional callback when splitbox resize stops 56 onResizeEnd: PropTypes.func, 57 // Retrieve DOM reference to the start panel element 58 onSelectContainerElement: PropTypes.any, 59 }; 60 } 61 62 static get defaultProps() { 63 return { 64 splitterSize: 5, 65 vert: true, 66 endPanelControl: false, 67 }; 68 } 69 70 static getDerivedStateFromProps(props, state) { 71 if ( 72 props.endPanelControl === state.prevEndPanelControl && 73 props.splitterSize === state.prevSplitterSize && 74 props.vert === state.prevVert 75 ) { 76 return null; 77 } 78 79 const newState = {}; 80 if (props.endPanelControl !== state.prevEndPanelControl) { 81 newState.endPanelControl = props.endPanelControl; 82 newState.prevEndPanelControl = props.endPanelControl; 83 } 84 if (props.splitterSize !== state.prevSplitterSize) { 85 newState.splitterSize = props.splitterSize; 86 newState.prevSplitterSize = props.splitterSize; 87 } 88 if (props.vert !== state.prevVert) { 89 newState.vert = props.vert; 90 newState.prevVert = props.vert; 91 } 92 93 return newState; 94 } 95 96 constructor(props) { 97 super(props); 98 99 /** 100 * The state stores whether or not the end panel should be controlled, the current 101 * orientation (vertical or horizontal), the splitter size, and the current size 102 * (width/height). All these values can change during the component's life time. 103 */ 104 this.state = { 105 // True if the right/bottom panel should be controlled. 106 endPanelControl: props.endPanelControl, 107 // True if the splitter bar is vertical (default is vertical). 108 vert: props.vert, 109 // Size of the splitter handle bar. 110 splitterSize: props.splitterSize, 111 // The state for above 3 properties are derived from props, but also managed by the component itself. 112 // SplitBox manages it's own state but sometimes the parent will pass in new props which will 113 // override the current state of the component. So we need track the prev value of these props so that 114 // compare them to the props change and derive new state whenever these 3 props change. 115 prevEndPanelControl: props.endPanelControl, 116 prevVert: props.vert, 117 prevSplitterSize: props.splitterSize, 118 // Width of controlled panel. 119 width: props.initialWidth || props.initialSize, 120 // Height of controlled panel. 121 height: props.initialHeight || props.initialSize, 122 }; 123 124 this.onStartMove = this.onStartMove.bind(this); 125 this.onStopMove = this.onStopMove.bind(this); 126 this.onMove = this.onMove.bind(this); 127 } 128 129 shouldComponentUpdate(nextProps, nextState) { 130 return ( 131 nextState.width != this.state.width || 132 nextState.endPanelControl != this.props.endPanelControl || 133 nextState.height != this.state.height || 134 nextState.vert != this.state.vert || 135 nextState.splitterSize != this.state.splitterSize || 136 nextProps.startPanel != this.props.startPanel || 137 nextProps.endPanel != this.props.endPanel || 138 nextProps.minSize != this.props.minSize || 139 nextProps.maxSize != this.props.maxSize 140 ); 141 } 142 143 componentDidUpdate(prevProps, prevState) { 144 if ( 145 this.props.onControlledPanelResized && 146 (prevState.width !== this.state.width || 147 prevState.height !== this.state.height) 148 ) { 149 this.props.onControlledPanelResized(this.state.width, this.state.height); 150 } 151 } 152 153 // Dragging Events 154 155 /** 156 * Set 'resizing' cursor on entire document during splitter dragging. 157 * This avoids cursor-flickering that happens when the mouse leaves 158 * the splitter bar area (happens frequently). 159 */ 160 onStartMove() { 161 const doc = this.splitBox.ownerDocument; 162 const defaultCursor = doc.documentElement.style.cursor; 163 doc.documentElement.style.cursor = this.state.vert 164 ? "ew-resize" 165 : "ns-resize"; 166 167 this.splitBox.classList.add("dragging"); 168 169 this.setState({ 170 defaultCursor, 171 }); 172 } 173 174 onStopMove() { 175 const doc = this.splitBox.ownerDocument; 176 doc.documentElement.style.cursor = this.state.defaultCursor; 177 178 this.splitBox.classList.remove("dragging"); 179 180 if (this.props.onResizeEnd) { 181 this.props.onResizeEnd( 182 this.state.vert ? this.state.width : this.state.height 183 ); 184 } 185 } 186 187 /** 188 * Adjust size of the controlled panel. Depending on the current 189 * orientation we either remember the width or height of 190 * the splitter box. 191 */ 192 onMove(x, y) { 193 const nodeBounds = this.splitBox.getBoundingClientRect(); 194 195 let size; 196 let { endPanelControl, vert } = this.state; 197 198 if (vert) { 199 // Use the document owning the SplitBox to detect rtl. The global document might be 200 // the one bound to the toolbox shared BrowserRequire, which is irrelevant here. 201 const doc = this.splitBox.ownerDocument; 202 203 // Switch the control flag in case of RTL. Note that RTL 204 // has impact on vertical splitter only. 205 if (doc.dir === "rtl") { 206 endPanelControl = !endPanelControl; 207 } 208 209 size = endPanelControl 210 ? nodeBounds.left + nodeBounds.width - x 211 : x - nodeBounds.left; 212 213 this.setState({ 214 width: this.getConstrainedSizeInPx(size, nodeBounds.width), 215 }); 216 } else { 217 size = endPanelControl 218 ? nodeBounds.top + nodeBounds.height - y 219 : y - nodeBounds.top; 220 221 this.setState({ 222 height: this.getConstrainedSizeInPx(size, nodeBounds.height), 223 }); 224 } 225 } 226 227 /** 228 * Calculates the constrained size taking into account the minimum width or 229 * height passed via this.props.minSize. 230 * 231 * @param {number} requestedSize 232 * The requested size 233 * @param {number} splitBoxWidthOrHeight 234 * The width or height of the splitBox 235 * 236 * @return {number} 237 * The constrained size 238 */ 239 getConstrainedSizeInPx(requestedSize, splitBoxWidthOrHeight) { 240 let minSize = this.props.minSize + ""; 241 242 if (minSize.endsWith("%")) { 243 minSize = (parseFloat(minSize) / 100) * splitBoxWidthOrHeight; 244 } else if (minSize.endsWith("px")) { 245 minSize = parseFloat(minSize); 246 } 247 return Math.max(requestedSize, minSize); 248 } 249 250 // Rendering 251 252 // eslint-disable-next-line complexity 253 render() { 254 const { endPanelControl, splitterSize, vert } = this.state; 255 const { 256 startPanel, 257 startPanelCollapsed, 258 endPanel, 259 endPanelCollapsed, 260 minSize, 261 maxSize, 262 onSelectContainerElement, 263 } = this.props; 264 265 const style = Object.assign( 266 { 267 // Set the size of the controlled panel (height or width depending on the 268 // current state). This can be used to help with styling of dependent 269 // panels. 270 "--split-box-controlled-panel-size": `${ 271 vert ? this.state.width : this.state.height 272 }`, 273 }, 274 this.props.style 275 ); 276 277 // Calculate class names list. 278 let classNames = ["split-box"]; 279 classNames.push(vert ? "vert" : "horz"); 280 if (this.props.className) { 281 classNames = classNames.concat(this.props.className.split(" ")); 282 } 283 284 let leftPanelStyle; 285 let rightPanelStyle; 286 287 // Set proper size for panels depending on the current state. 288 if (vert) { 289 leftPanelStyle = { 290 maxWidth: endPanelControl ? null : maxSize, 291 minWidth: endPanelControl ? null : minSize, 292 width: endPanelControl ? null : this.state.width, 293 }; 294 rightPanelStyle = { 295 maxWidth: endPanelControl ? maxSize : null, 296 minWidth: endPanelControl ? minSize : null, 297 width: endPanelControl ? this.state.width : null, 298 }; 299 } else { 300 leftPanelStyle = { 301 maxHeight: endPanelControl ? null : maxSize, 302 minHeight: endPanelControl ? null : minSize, 303 height: endPanelControl ? null : this.state.height, 304 }; 305 rightPanelStyle = { 306 maxHeight: endPanelControl ? maxSize : null, 307 minHeight: endPanelControl ? minSize : null, 308 height: endPanelControl ? this.state.height : null, 309 }; 310 } 311 312 // Calculate splitter size 313 const splitterStyle = { 314 flex: "0 0 " + splitterSize + "px", 315 }; 316 317 return dom.div( 318 { 319 className: classNames.join(" "), 320 ref: div => { 321 this.splitBox = div; 322 }, 323 style, 324 }, 325 startPanel && !startPanelCollapsed 326 ? dom.div( 327 { 328 className: endPanelControl ? "uncontrolled" : "controlled", 329 style: leftPanelStyle, 330 role: "presentation", 331 ref: div => { 332 this.startPanelContainer = div; 333 if (onSelectContainerElement) { 334 onSelectContainerElement(div); 335 } 336 }, 337 }, 338 startPanel 339 ) 340 : null, 341 splitterSize > 0 342 ? Draggable({ 343 className: "splitter", 344 style: splitterStyle, 345 onStart: this.onStartMove, 346 onStop: this.onStopMove, 347 onMove: this.onMove, 348 }) 349 : null, 350 endPanel && !endPanelCollapsed 351 ? dom.div( 352 { 353 className: endPanelControl ? "controlled" : "uncontrolled", 354 style: rightPanelStyle, 355 role: "presentation", 356 ref: div => { 357 this.endPanelContainer = div; 358 }, 359 }, 360 endPanel 361 ) 362 : null 363 ); 364 } 365 } 366 367 module.exports = SplitBox;