Tabs.js (8335B)
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 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import React, { PureComponent } from "devtools/client/shared/vendor/react"; 6 import { 7 div, 8 ul, 9 li, 10 span, 11 } from "devtools/client/shared/vendor/react-dom-factories"; 12 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 13 import { connect } from "devtools/client/shared/vendor/react-redux"; 14 15 import { 16 getOpenedSources, 17 getSelectedSource, 18 getIsPaused, 19 getCurrentThread, 20 getBlackBoxRanges, 21 } from "../../selectors/index"; 22 import { isVisible } from "../../utils/ui"; 23 24 import { getHiddenTabsSources } from "../../utils/tabs"; 25 import { getFileURL } from "../../utils/source"; 26 import actions from "../../actions/index"; 27 28 import Tab from "./Tab"; 29 import { PaneToggleButton } from "../shared/Button/index"; 30 import Dropdown from "../shared/Dropdown"; 31 import DebuggerImage from "../shared/DebuggerImage"; 32 import CommandBar from "../SecondaryPanes/CommandBar"; 33 34 const { debounce } = require("resource://devtools/shared/debounce.js"); 35 36 class Tabs extends PureComponent { 37 constructor(props) { 38 super(props); 39 this.state = { 40 dropdownShown: false, 41 // List of Sources objects for the tabs that overflow and are shown in the drop down menu 42 hiddenSources: [], 43 }; 44 45 this.onResize = debounce(() => { 46 this.updateHiddenTabs(); 47 }); 48 } 49 50 static get propTypes() { 51 return { 52 endPanelCollapsed: PropTypes.bool.isRequired, 53 horizontal: PropTypes.bool.isRequired, 54 isPaused: PropTypes.bool.isRequired, 55 moveTab: PropTypes.func.isRequired, 56 moveTabBySourceId: PropTypes.func.isRequired, 57 selectSource: PropTypes.func.isRequired, 58 selectedSource: PropTypes.object, 59 blackBoxRanges: PropTypes.object.isRequired, 60 startPanelCollapsed: PropTypes.bool.isRequired, 61 openedSources: PropTypes.array.isRequired, 62 togglePaneCollapse: PropTypes.func.isRequired, 63 }; 64 } 65 66 componentDidUpdate(prevProps) { 67 if ( 68 this.props.selectedSource !== prevProps.selectedSource || 69 this.props.openedSources !== prevProps.openedSources 70 ) { 71 this.updateHiddenTabs(); 72 } 73 } 74 75 componentDidMount() { 76 window.requestIdleCallback(this.updateHiddenTabs); 77 window.addEventListener("resize", this.onResize); 78 window.document 79 .querySelector(".editor-pane") 80 .addEventListener("resizeend", this.onResize); 81 } 82 83 componentWillUnmount() { 84 window.removeEventListener("resize", this.onResize); 85 window.document 86 .querySelector(".editor-pane") 87 .removeEventListener("resizeend", this.onResize); 88 } 89 90 /* 91 * Updates the hiddenSourceTabs state, by 92 * finding the source tabs which are wrapped and are not on the top row. 93 */ 94 updateHiddenTabs = () => { 95 if (!this.refs.sourceTabs) { 96 // Ensure hiding the dropdown if we removed all sources. 97 if (this.state.hiddenSources.length) { 98 this.setState({ hiddenSources: [] }); 99 } 100 return; 101 } 102 const { selectedSource, moveTab } = this.props; 103 const sourceTabEls = this.refs.sourceTabs.children; 104 const hiddenSources = getHiddenTabsSources( 105 this.props.openedSources, 106 sourceTabEls 107 ); 108 109 if ( 110 selectedSource && 111 isVisible() && 112 hiddenSources.includes(selectedSource) 113 ) { 114 moveTab(selectedSource.url, 0); 115 return; 116 } 117 118 this.setState({ hiddenSources }); 119 }; 120 121 toggleSourcesDropdown() { 122 this.setState(prevState => ({ 123 dropdownShown: !prevState.dropdownShown, 124 })); 125 } 126 127 getIconClass(source) { 128 if (this.props.blackBoxRanges[source.url]) { 129 return "blackBox"; 130 } 131 return "file"; 132 } 133 134 renderDropdownSource = source => { 135 const { selectSource } = this.props; 136 137 const onClick = () => selectSource(source); 138 return li( 139 { 140 key: source.id, 141 onClick, 142 title: getFileURL(source, false), 143 }, 144 React.createElement(DebuggerImage, { 145 name: this.getIconClass(source), 146 className: "dropdown-icon", 147 }), 148 span( 149 { 150 className: "dropdown-label", 151 }, 152 source.shortName 153 ) 154 ); 155 }; 156 157 // Note that these three listener will be called from Tab component 158 // so that e.target will be Tab's DOM (and not Tabs one). 159 onTabDragStart = e => { 160 this.draggedSourceId = e.target.dataset.sourceId; 161 this.draggedSourceIndex = e.target.dataset.index; 162 }; 163 164 onTabDragEnd = () => { 165 this.draggedSourceId = null; 166 this.draggedSourceIndex = -1; 167 }; 168 169 onTabDragOver = e => { 170 e.preventDefault(); 171 172 const hoveredTabIndex = e.target.dataset.index; 173 const { moveTabBySourceId } = this.props; 174 175 if (hoveredTabIndex === this.draggedSourceIndex) { 176 return; 177 } 178 179 const tabDOMRect = e.target.getBoundingClientRect(); 180 const { pageX: mouseCursorX } = e; 181 if ( 182 /* Case: the mouse cursor moves into the left half of any target tab */ 183 mouseCursorX - tabDOMRect.left < 184 tabDOMRect.width / 2 185 ) { 186 // The current tab goes to the left of the target tab 187 const targetTab = 188 hoveredTabIndex > this.draggedSourceIndex 189 ? hoveredTabIndex - 1 190 : hoveredTabIndex; 191 moveTabBySourceId(this.draggedSourceId, targetTab); 192 this.draggedSourceIndex = targetTab; 193 } else if ( 194 /* Case: the mouse cursor moves into the right half of any target tab */ 195 mouseCursorX - tabDOMRect.left >= 196 tabDOMRect.width / 2 197 ) { 198 // The current tab goes to the right of the target tab 199 const targetTab = 200 hoveredTabIndex < this.draggedSourceIndex 201 ? hoveredTabIndex + 1 202 : hoveredTabIndex; 203 moveTabBySourceId(this.draggedSourceId, targetTab); 204 this.draggedSourceIndex = targetTab; 205 } 206 }; 207 208 renderTabs() { 209 const { openedSources } = this.props; 210 if (!openedSources.length) { 211 return null; 212 } 213 return div( 214 { 215 className: "source-tabs", 216 ref: "sourceTabs", 217 }, 218 openedSources.map((source, index) => { 219 return React.createElement(Tab, { 220 onDragStart: this.onTabDragStart, 221 onDragOver: this.onTabDragOver, 222 onDragEnd: this.onTabDragEnd, 223 key: source.id, 224 index, 225 source, 226 }); 227 }) 228 ); 229 } 230 231 renderDropdown() { 232 const { hiddenSources } = this.state; 233 if (!hiddenSources || !hiddenSources.length) { 234 return null; 235 } 236 const panel = ul(null, hiddenSources.map(this.renderDropdownSource)); 237 const icon = React.createElement(DebuggerImage, { 238 name: "more-tabs", 239 }); 240 return React.createElement(Dropdown, { 241 panel, 242 icon, 243 }); 244 } 245 246 renderCommandBar() { 247 const { horizontal, endPanelCollapsed, isPaused } = this.props; 248 if (!endPanelCollapsed || !isPaused) { 249 return null; 250 } 251 return React.createElement(CommandBar, { 252 horizontal, 253 }); 254 } 255 256 renderStartPanelToggleButton() { 257 return React.createElement(PaneToggleButton, { 258 position: "start", 259 collapsed: this.props.startPanelCollapsed, 260 handleClick: this.props.togglePaneCollapse, 261 }); 262 } 263 264 renderEndPanelToggleButton() { 265 const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props; 266 if (!horizontal) { 267 return null; 268 } 269 return React.createElement(PaneToggleButton, { 270 position: "end", 271 collapsed: endPanelCollapsed, 272 handleClick: togglePaneCollapse, 273 horizontal, 274 }); 275 } 276 277 render() { 278 return div( 279 { 280 className: "source-header", 281 }, 282 this.renderStartPanelToggleButton(), 283 this.renderTabs(), 284 this.renderDropdown(), 285 this.renderEndPanelToggleButton(), 286 this.renderCommandBar() 287 ); 288 } 289 } 290 291 const mapStateToProps = state => { 292 return { 293 selectedSource: getSelectedSource(state), 294 openedSources: getOpenedSources(state), 295 blackBoxRanges: getBlackBoxRanges(state), 296 isPaused: getIsPaused(state, getCurrentThread(state)), 297 }; 298 }; 299 300 export default connect(mapStateToProps, { 301 selectSource: actions.selectSource, 302 moveTab: actions.moveTab, 303 moveTabBySourceId: actions.moveTabBySourceId, 304 togglePaneCollapse: actions.togglePaneCollapse, 305 })(Tabs);