RequestBlockingPanel.js (9915B)
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 "use strict"; 6 7 const { 8 Component, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const { 11 button, 12 div, 13 form, 14 input, 15 label, 16 li, 17 span, 18 ul, 19 } = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 20 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 21 const { 22 connect, 23 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 24 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 25 const { 26 L10N, 27 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 28 const { 29 PANELS, 30 } = require("resource://devtools/client/netmonitor/src/constants.js"); 31 32 const RequestBlockingContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js"); 33 34 const ENABLE_BLOCKING_LABEL = L10N.getStr( 35 "netmonitor.actionbar.enableBlocking" 36 ); 37 const ADD_URL_PLACEHOLDER = L10N.getStr( 38 "netmonitor.actionbar.blockSearchPlaceholder" 39 ); 40 const REQUEST_BLOCKING_USAGE_NOTICE = L10N.getStr( 41 "netmonitor.actionbar.requestBlockingUsageNotice" 42 ); 43 const REQUEST_BLOCKING_ADD_NOTICE = L10N.getStr( 44 "netmonitor.actionbar.requestBlockingAddNotice" 45 ); 46 const REMOVE_URL_TOOLTIP = L10N.getStr("netmonitor.actionbar.removeBlockedUrl"); 47 48 class RequestBlockingPanel extends Component { 49 static get propTypes() { 50 return { 51 blockedUrls: PropTypes.array.isRequired, 52 addBlockedUrl: PropTypes.func.isRequired, 53 isDisplaying: PropTypes.bool.isRequired, 54 removeBlockedUrl: PropTypes.func.isRequired, 55 toggleBlockingEnabled: PropTypes.func.isRequired, 56 toggleBlockedUrl: PropTypes.func.isRequired, 57 updateBlockedUrl: PropTypes.func.isRequired, 58 removeAllBlockedUrls: PropTypes.func.isRequired, 59 disableAllBlockedUrls: PropTypes.func.isRequired, 60 enableAllBlockedUrls: PropTypes.func.isRequired, 61 blockingEnabled: PropTypes.bool.isRequired, 62 }; 63 } 64 65 constructor(props) { 66 super(props); 67 68 this.state = { 69 editingUrl: null, 70 }; 71 } 72 73 componentDidMount() { 74 this.refs.addInput.focus(); 75 } 76 77 componentDidUpdate(prevProps) { 78 if (this.state.editingUrl) { 79 this.refs.editInput.focus(); 80 this.refs.editInput.select(); 81 } else if (this.props.isDisplaying && !prevProps.isDisplaying) { 82 this.refs.addInput.focus(); 83 } 84 } 85 86 componentWillUnmount() { 87 if (this.scrollToBottomTimeout) { 88 clearTimeout(this.scrollToBottomTimeout); 89 } 90 } 91 92 scrollToBottom() { 93 if (this.scrollToBottomTimeout) { 94 clearTimeout(this.scrollToBottomTimeout); 95 } 96 this.scrollToBottomTimeout = setTimeout(() => { 97 const { contents } = this.refs; 98 if (contents.scrollHeight > contents.offsetHeight) { 99 contents.scrollTo({ top: contents.scrollHeight }); 100 } 101 }, 40); 102 } 103 104 renderEnableBar() { 105 return div( 106 { className: "request-blocking-enable-bar" }, 107 div( 108 { className: "request-blocking-enable-form" }, 109 label( 110 { className: "devtools-checkbox-label" }, 111 input({ 112 type: "checkbox", 113 className: "devtools-checkbox", 114 checked: this.props.blockingEnabled, 115 ref: "enabledCheckbox", 116 onChange: () => 117 this.props.toggleBlockingEnabled( 118 this.refs.enabledCheckbox.checked 119 ), 120 }), 121 span({ className: "request-blocking-label" }, ENABLE_BLOCKING_LABEL) 122 ) 123 ) 124 ); 125 } 126 127 renderItemContent({ url, enabled }) { 128 const { toggleBlockedUrl, removeBlockedUrl } = this.props; 129 130 return li( 131 { key: url }, 132 label( 133 { 134 className: "devtools-checkbox-label", 135 onDoubleClick: () => this.setState({ editingUrl: url }), 136 }, 137 input({ 138 type: "checkbox", 139 className: "devtools-checkbox", 140 checked: enabled, 141 onChange: () => toggleBlockedUrl(url), 142 }), 143 span( 144 { 145 className: "request-blocking-label request-blocking-editable-label", 146 title: url, 147 }, 148 url 149 ) 150 ), 151 button({ 152 className: "request-blocking-remove-button", 153 title: REMOVE_URL_TOOLTIP, 154 "aria-label": REMOVE_URL_TOOLTIP, 155 onClick: () => removeBlockedUrl(url), 156 }) 157 ); 158 } 159 160 renderEditForm(url) { 161 const { updateBlockedUrl, removeBlockedUrl } = this.props; 162 return li( 163 { key: url, className: "request-blocking-edit-item" }, 164 form( 165 { 166 onSubmit: e => { 167 const { editInput } = this.refs; 168 const newValue = editInput.value; 169 e.preventDefault(); 170 171 if (url != newValue) { 172 if (editInput.value.trim() === "") { 173 removeBlockedUrl(url, newValue); 174 } else { 175 updateBlockedUrl(url, newValue); 176 } 177 } 178 this.setState({ editingUrl: null }); 179 }, 180 }, 181 input({ 182 type: "text", 183 defaultValue: url, 184 ref: "editInput", 185 className: "devtools-searchinput", 186 placeholder: ADD_URL_PLACEHOLDER, 187 onBlur: () => this.setState({ editingUrl: null }), 188 onKeyDown: e => { 189 if (e.key === "Escape") { 190 e.stopPropagation(); 191 e.preventDefault(); 192 this.setState({ editingUrl: null }); 193 } 194 }, 195 }), 196 197 input({ type: "submit", style: { display: "none" } }) 198 ) 199 ); 200 } 201 202 renderBlockedList() { 203 const { 204 blockedUrls, 205 blockingEnabled, 206 removeAllBlockedUrls, 207 disableAllBlockedUrls, 208 enableAllBlockedUrls, 209 } = this.props; 210 211 if (blockedUrls.length === 0) { 212 return null; 213 } 214 215 const listItems = blockedUrls.map(item => 216 this.state.editingUrl === item.url 217 ? this.renderEditForm(item.url) 218 : this.renderItemContent(item) 219 ); 220 221 return div( 222 { 223 className: "request-blocking-contents", 224 ref: "contents", 225 onContextMenu: event => { 226 if (!this.contextMenu) { 227 this.contextMenu = new RequestBlockingContextMenu({ 228 removeAllBlockedUrls, 229 disableAllBlockedUrls, 230 enableAllBlockedUrls, 231 }); 232 } 233 234 const contextMenuOptions = { 235 disableDisableAllBlockedUrls: blockedUrls.every( 236 ({ enabled }) => enabled === false 237 ), 238 disableEnableAllBlockedUrls: blockedUrls.every( 239 ({ enabled }) => enabled === true 240 ), 241 }; 242 243 this.contextMenu.open(event, contextMenuOptions); 244 }, 245 }, 246 ul( 247 { 248 className: `request-blocking-list ${ 249 blockingEnabled ? "" : "disabled" 250 }`, 251 }, 252 ...listItems 253 ) 254 ); 255 } 256 257 renderAddForm() { 258 const { addBlockedUrl } = this.props; 259 return div( 260 { className: "request-blocking-footer" }, 261 form( 262 { 263 className: "request-blocking-add-form", 264 onSubmit: e => { 265 const { addInput } = this.refs; 266 e.preventDefault(); 267 addBlockedUrl(addInput.value); 268 addInput.value = ""; 269 addInput.focus(); 270 this.scrollToBottom(); 271 }, 272 }, 273 input({ 274 type: "text", 275 ref: "addInput", 276 className: "devtools-searchinput", 277 placeholder: ADD_URL_PLACEHOLDER, 278 onKeyDown: e => { 279 if (e.key === "Escape") { 280 e.stopPropagation(); 281 e.preventDefault(); 282 283 const { addInput } = this.refs; 284 addInput.value = ""; 285 addInput.focus(); 286 } 287 }, 288 }), 289 input({ type: "submit", style: { display: "none" } }) 290 ) 291 ); 292 } 293 294 renderEmptyListNotice() { 295 return div( 296 { className: "request-blocking-list-empty-notice" }, 297 div( 298 { className: "request-blocking-notice-element" }, 299 REQUEST_BLOCKING_USAGE_NOTICE 300 ), 301 div( 302 { className: "request-blocking-notice-element" }, 303 REQUEST_BLOCKING_ADD_NOTICE 304 ) 305 ); 306 } 307 308 render() { 309 const { blockedUrls, addBlockedUrl } = this.props; 310 311 return div( 312 { 313 className: "request-blocking-panel", 314 onDragOver: e => { 315 e.preventDefault(); 316 }, 317 onDrop: e => { 318 e.preventDefault(); 319 const url = e.dataTransfer.getData("text/plain"); 320 addBlockedUrl(url); 321 this.scrollToBottom(); 322 }, 323 }, 324 this.renderEnableBar(), 325 this.renderBlockedList(), 326 this.renderAddForm(), 327 !blockedUrls.length && this.renderEmptyListNotice() 328 ); 329 } 330 } 331 332 module.exports = connect( 333 state => ({ 334 blockedUrls: state.requestBlocking.blockedUrls, 335 blockingEnabled: state.requestBlocking.blockingEnabled, 336 isDisplaying: state.ui.selectedActionBarTabId === PANELS.BLOCKING, 337 }), 338 dispatch => ({ 339 toggleBlockingEnabled: checked => 340 dispatch(Actions.toggleBlockingEnabled(checked)), 341 addBlockedUrl: url => dispatch(Actions.addBlockedUrl(url)), 342 removeBlockedUrl: url => dispatch(Actions.removeBlockedUrl(url)), 343 toggleBlockedUrl: url => dispatch(Actions.toggleBlockedUrl(url)), 344 removeAllBlockedUrls: () => dispatch(Actions.removeAllBlockedUrls()), 345 enableAllBlockedUrls: () => dispatch(Actions.enableAllBlockedUrls()), 346 disableAllBlockedUrls: () => dispatch(Actions.disableAllBlockedUrls()), 347 updateBlockedUrl: (oldUrl, newUrl) => 348 dispatch(Actions.updateBlockedUrl(oldUrl, newUrl)), 349 }) 350 )(RequestBlockingPanel);