/* 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 . */ import React, { Component } from "devtools/client/shared/vendor/react"; import { div, input, li, ul, span, button, form, label, } from "devtools/client/shared/vendor/react-dom-factories"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import { connect } from "devtools/client/shared/vendor/react-redux"; import actions from "../../actions/index"; import { getActiveEventListeners, getEventListenerBreakpointTypes, getEventListenerExpanded, } from "../../selectors/index"; import AccessibleImage from "../shared/AccessibleImage"; const classnames = require("resource://devtools/client/shared/classnames.js"); class EventListeners extends Component { state = { searchText: "", focused: false, }; static get propTypes() { return { activeEventListeners: PropTypes.array.isRequired, addEventListenerExpanded: PropTypes.func.isRequired, addEventListeners: PropTypes.func.isRequired, categories: PropTypes.array.isRequired, expandedCategories: PropTypes.array.isRequired, removeEventListenerExpanded: PropTypes.func.isRequired, removeEventListeners: PropTypes.func.isRequired, }; } hasMatch(eventOrCategoryName, searchText) { const lowercaseEventOrCategoryName = eventOrCategoryName.toLowerCase(); const lowercaseSearchText = searchText.toLowerCase(); return lowercaseEventOrCategoryName.includes(lowercaseSearchText); } getSearchResults() { const { searchText } = this.state; const { categories } = this.props; const searchResults = categories.reduce((results, cat, index) => { const category = categories[index]; if (this.hasMatch(category.name, searchText)) { results[category.name] = category.events; } else { results[category.name] = category.events.filter(event => this.hasMatch(event.name, searchText) ); } return results; }, {}); return searchResults; } onCategoryToggle(category) { const { expandedCategories, removeEventListenerExpanded, addEventListenerExpanded, } = this.props; if (expandedCategories.includes(category)) { removeEventListenerExpanded(category); } else { addEventListenerExpanded(category); } } onCategoryClick(category, isChecked) { const { addEventListeners, removeEventListeners } = this.props; const eventsIds = category.events.map(event => event.id); if (isChecked) { addEventListeners(eventsIds); } else { removeEventListeners(eventsIds); } } onEventTypeClick(eventId, isChecked) { const { addEventListeners, removeEventListeners } = this.props; if (isChecked) { addEventListeners([eventId]); } else { removeEventListeners([eventId]); } } onInputChange = event => { this.setState({ searchText: event.currentTarget.value }); }; onKeyDown = event => { if (event.key === "Escape") { this.setState({ searchText: "" }); } }; onFocus = () => { this.setState({ focused: true }); }; onBlur = () => { this.setState({ focused: false }); }; renderSearchInput() { const { focused, searchText } = this.state; const placeholder = L10N.getStr("eventListenersHeader1.placeholder"); return form( { className: "event-search-form", onSubmit: e => e.preventDefault(), }, input({ className: classnames("event-search-input", { focused, }), placeholder, value: searchText, onChange: this.onInputChange, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, }) ); } renderClearSearchButton() { const { searchText } = this.state; if (!searchText) { return null; } return button({ onClick: () => this.setState({ searchText: "", }), className: "devtools-searchinput-clear", }); } renderCategoriesList() { const { categories } = this.props; return ul( { className: "event-listeners-list", }, categories.map((category, index) => { return li( { className: "event-listener-group", key: index, }, this.renderCategoryHeading(category), this.renderCategoryListing(category) ); }) ); } renderSearchResultsList() { const searchResults = this.getSearchResults(); return ul( { className: "event-search-results-list", }, Object.keys(searchResults).map(category => { return searchResults[category].map(event => { return this.renderListenerEvent(event, category); }); }) ); } renderCategoryHeading(category) { const { activeEventListeners, expandedCategories } = this.props; const { events } = category; const expanded = expandedCategories.includes(category.name); const checked = events.every(({ id }) => activeEventListeners.includes(id)); const indeterminate = !checked && events.some(({ id }) => activeEventListeners.includes(id)); return div( { className: "event-listener-header", }, button( { className: "event-listener-expand", onClick: () => this.onCategoryToggle(category.name), }, React.createElement(AccessibleImage, { className: classnames("arrow", { expanded, }), }) ), label( { className: "event-listener-label", }, input({ type: "checkbox", value: category.name, onChange: e => { this.onCategoryClick( category, // Clicking an indeterminate checkbox should always have the // effect of disabling any selected items. indeterminate ? false : e.target.checked ); }, checked, ref: el => el && (el.indeterminate = indeterminate), }), span( { className: "event-listener-category", }, category.name ) ) ); } renderCategoryListing(category) { const { expandedCategories } = this.props; const expanded = expandedCategories.includes(category.name); if (!expanded) { return null; } return ul( null, category.events.map(event => { return this.renderListenerEvent(event, category.name); }) ); } renderCategory(category) { return span( { className: "category-label", }, category, " \u25B8 " ); } renderListenerEvent(event, category) { const { activeEventListeners } = this.props; const { searchText } = this.state; return li( { className: "event-listener-event", key: event.id, }, label( { className: "event-listener-label", }, input({ type: "checkbox", value: event.id, onChange: e => this.onEventTypeClick(event.id, e.target.checked), checked: activeEventListeners.includes(event.id), }), span( { className: "event-listener-name", }, searchText ? this.renderCategory(category) : null, event.name ) ) ); } render() { const { searchText } = this.state; return div( { className: "event-listeners", }, div( { className: "event-search-container", }, this.renderSearchInput(), this.renderClearSearchButton() ), div( { className: "event-listeners-content", }, searchText ? this.renderSearchResultsList() : this.renderCategoriesList() ) ); } } const mapStateToProps = state => ({ activeEventListeners: getActiveEventListeners(state), categories: getEventListenerBreakpointTypes(state), expandedCategories: getEventListenerExpanded(state), }); export default connect(mapStateToProps, { addEventListeners: actions.addEventListenerBreakpoints, removeEventListeners: actions.removeEventListenerBreakpoints, addEventListenerExpanded: actions.addEventListenerExpanded, removeEventListenerExpanded: actions.removeEventListenerExpanded, })(EventListeners);