/* 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, } = require("resource://devtools/client/shared/vendor/react.js"); const { button, div, form, input, label, li, span, ul, } = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const { connect, } = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); const { L10N, } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); const { PANELS, } = require("resource://devtools/client/netmonitor/src/constants.js"); const RequestBlockingContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js"); const ENABLE_BLOCKING_LABEL = L10N.getStr( "netmonitor.actionbar.enableBlocking" ); const ADD_URL_PLACEHOLDER = L10N.getStr( "netmonitor.actionbar.blockSearchPlaceholder" ); const REQUEST_BLOCKING_USAGE_NOTICE = L10N.getStr( "netmonitor.actionbar.requestBlockingUsageNotice" ); const REQUEST_BLOCKING_ADD_NOTICE = L10N.getStr( "netmonitor.actionbar.requestBlockingAddNotice" ); const REMOVE_URL_TOOLTIP = L10N.getStr("netmonitor.actionbar.removeBlockedUrl"); class RequestBlockingPanel extends Component { static get propTypes() { return { blockedUrls: PropTypes.array.isRequired, addBlockedUrl: PropTypes.func.isRequired, isDisplaying: PropTypes.bool.isRequired, removeBlockedUrl: PropTypes.func.isRequired, toggleBlockingEnabled: PropTypes.func.isRequired, toggleBlockedUrl: PropTypes.func.isRequired, updateBlockedUrl: PropTypes.func.isRequired, removeAllBlockedUrls: PropTypes.func.isRequired, disableAllBlockedUrls: PropTypes.func.isRequired, enableAllBlockedUrls: PropTypes.func.isRequired, blockingEnabled: PropTypes.bool.isRequired, }; } constructor(props) { super(props); this.state = { editingUrl: null, }; } componentDidMount() { this.refs.addInput.focus(); } componentDidUpdate(prevProps) { if (this.state.editingUrl) { this.refs.editInput.focus(); this.refs.editInput.select(); } else if (this.props.isDisplaying && !prevProps.isDisplaying) { this.refs.addInput.focus(); } } componentWillUnmount() { if (this.scrollToBottomTimeout) { clearTimeout(this.scrollToBottomTimeout); } } scrollToBottom() { if (this.scrollToBottomTimeout) { clearTimeout(this.scrollToBottomTimeout); } this.scrollToBottomTimeout = setTimeout(() => { const { contents } = this.refs; if (contents.scrollHeight > contents.offsetHeight) { contents.scrollTo({ top: contents.scrollHeight }); } }, 40); } renderEnableBar() { return div( { className: "request-blocking-enable-bar" }, div( { className: "request-blocking-enable-form" }, label( { className: "devtools-checkbox-label" }, input({ type: "checkbox", className: "devtools-checkbox", checked: this.props.blockingEnabled, ref: "enabledCheckbox", onChange: () => this.props.toggleBlockingEnabled( this.refs.enabledCheckbox.checked ), }), span({ className: "request-blocking-label" }, ENABLE_BLOCKING_LABEL) ) ) ); } renderItemContent({ url, enabled }) { const { toggleBlockedUrl, removeBlockedUrl } = this.props; return li( { key: url }, label( { className: "devtools-checkbox-label", onDoubleClick: () => this.setState({ editingUrl: url }), }, input({ type: "checkbox", className: "devtools-checkbox", checked: enabled, onChange: () => toggleBlockedUrl(url), }), span( { className: "request-blocking-label request-blocking-editable-label", title: url, }, url ) ), button({ className: "request-blocking-remove-button", title: REMOVE_URL_TOOLTIP, "aria-label": REMOVE_URL_TOOLTIP, onClick: () => removeBlockedUrl(url), }) ); } renderEditForm(url) { const { updateBlockedUrl, removeBlockedUrl } = this.props; return li( { key: url, className: "request-blocking-edit-item" }, form( { onSubmit: e => { const { editInput } = this.refs; const newValue = editInput.value; e.preventDefault(); if (url != newValue) { if (editInput.value.trim() === "") { removeBlockedUrl(url, newValue); } else { updateBlockedUrl(url, newValue); } } this.setState({ editingUrl: null }); }, }, input({ type: "text", defaultValue: url, ref: "editInput", className: "devtools-searchinput", placeholder: ADD_URL_PLACEHOLDER, onBlur: () => this.setState({ editingUrl: null }), onKeyDown: e => { if (e.key === "Escape") { e.stopPropagation(); e.preventDefault(); this.setState({ editingUrl: null }); } }, }), input({ type: "submit", style: { display: "none" } }) ) ); } renderBlockedList() { const { blockedUrls, blockingEnabled, removeAllBlockedUrls, disableAllBlockedUrls, enableAllBlockedUrls, } = this.props; if (blockedUrls.length === 0) { return null; } const listItems = blockedUrls.map(item => this.state.editingUrl === item.url ? this.renderEditForm(item.url) : this.renderItemContent(item) ); return div( { className: "request-blocking-contents", ref: "contents", onContextMenu: event => { if (!this.contextMenu) { this.contextMenu = new RequestBlockingContextMenu({ removeAllBlockedUrls, disableAllBlockedUrls, enableAllBlockedUrls, }); } const contextMenuOptions = { disableDisableAllBlockedUrls: blockedUrls.every( ({ enabled }) => enabled === false ), disableEnableAllBlockedUrls: blockedUrls.every( ({ enabled }) => enabled === true ), }; this.contextMenu.open(event, contextMenuOptions); }, }, ul( { className: `request-blocking-list ${ blockingEnabled ? "" : "disabled" }`, }, ...listItems ) ); } renderAddForm() { const { addBlockedUrl } = this.props; return div( { className: "request-blocking-footer" }, form( { className: "request-blocking-add-form", onSubmit: e => { const { addInput } = this.refs; e.preventDefault(); addBlockedUrl(addInput.value); addInput.value = ""; addInput.focus(); this.scrollToBottom(); }, }, input({ type: "text", ref: "addInput", className: "devtools-searchinput", placeholder: ADD_URL_PLACEHOLDER, onKeyDown: e => { if (e.key === "Escape") { e.stopPropagation(); e.preventDefault(); const { addInput } = this.refs; addInput.value = ""; addInput.focus(); } }, }), input({ type: "submit", style: { display: "none" } }) ) ); } renderEmptyListNotice() { return div( { className: "request-blocking-list-empty-notice" }, div( { className: "request-blocking-notice-element" }, REQUEST_BLOCKING_USAGE_NOTICE ), div( { className: "request-blocking-notice-element" }, REQUEST_BLOCKING_ADD_NOTICE ) ); } render() { const { blockedUrls, addBlockedUrl } = this.props; return div( { className: "request-blocking-panel", onDragOver: e => { e.preventDefault(); }, onDrop: e => { e.preventDefault(); const url = e.dataTransfer.getData("text/plain"); addBlockedUrl(url); this.scrollToBottom(); }, }, this.renderEnableBar(), this.renderBlockedList(), this.renderAddForm(), !blockedUrls.length && this.renderEmptyListNotice() ); } } module.exports = connect( state => ({ blockedUrls: state.requestBlocking.blockedUrls, blockingEnabled: state.requestBlocking.blockingEnabled, isDisplaying: state.ui.selectedActionBarTabId === PANELS.BLOCKING, }), dispatch => ({ toggleBlockingEnabled: checked => dispatch(Actions.toggleBlockingEnabled(checked)), addBlockedUrl: url => dispatch(Actions.addBlockedUrl(url)), removeBlockedUrl: url => dispatch(Actions.removeBlockedUrl(url)), toggleBlockedUrl: url => dispatch(Actions.toggleBlockedUrl(url)), removeAllBlockedUrls: () => dispatch(Actions.removeAllBlockedUrls()), enableAllBlockedUrls: () => dispatch(Actions.enableAllBlockedUrls()), disableAllBlockedUrls: () => dispatch(Actions.disableAllBlockedUrls()), updateBlockedUrl: (oldUrl, newUrl) => dispatch(Actions.updateBlockedUrl(oldUrl, newUrl)), }) )(RequestBlockingPanel);