Source code

Revision control

Copy as Markdown

Other Tools

/* 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/>. */
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);