diff --git a/devtools/client/debugger/src/actions/event-listeners.js b/devtools/client/debugger/src/actions/event-listeners.js index 0e47899bf087..4e1e41e321f7 100644 --- a/devtools/client/debugger/src/actions/event-listeners.js +++ b/devtools/client/debugger/src/actions/event-listeners.js @@ -4,29 +4,88 @@ // @flow +import { uniq, remove } from "lodash"; + import { asyncStore } from "../utils/prefs"; +import { + getActiveEventListeners, + getEventListenerExpanded, +} from "../selectors"; + import type { ThunkArgs } from "./types"; -import type { EventListenerBreakpoints } from "../types"; -export function addEventListeners(events: EventListenerBreakpoints) { - return async ({ dispatch, client }: ThunkArgs) => { - await dispatch({ - type: "ADD_EVENT_LISTENERS", - events, - }); - const newList = await asyncStore.eventListenerBreakpoints; - client.setEventListenerBreakpoints(newList); +async function updateBreakpoints(dispatch, client, newEvents: string[]) { + dispatch({ type: "UPDATE_EVENT_LISTENERS", active: newEvents }); + + const current = await asyncStore.eventListenerBreakpoints; + asyncStore.eventListenerBreakpoints = { + ...current, + active: newEvents, + }; + + client.setEventListenerBreakpoints(newEvents); +} + +async function updateExpanded(dispatch, newExpanded: string[]) { + dispatch({ + type: "UPDATE_EVENT_LISTENER_EXPANDED", + expanded: newExpanded, + }); + + const current = await asyncStore.eventListenerBreakpoints; + asyncStore.eventListenerBreakpoints = { + ...current, + expanded: newExpanded, }; } -export function removeEventListeners(events: EventListenerBreakpoints) { - return async ({ dispatch, client }: ThunkArgs) => { - await dispatch({ - type: "REMOVE_EVENT_LISTENERS", - events, - }); - const newList = await asyncStore.eventListenerBreakpoints; - client.setEventListenerBreakpoints(newList); +export function addEventListenerBreakpoints(eventsToAdd: string[]) { + return async ({ dispatch, client, getState }: ThunkArgs) => { + const activeListenerBreakpoints = await getActiveEventListeners(getState()); + + const newEvents = uniq([...eventsToAdd, ...activeListenerBreakpoints]); + + updateBreakpoints(dispatch, client, newEvents); + }; +} + +export function removeEventListenerBreakpoints(eventsToRemove: string[]) { + return async ({ dispatch, client, getState }: ThunkArgs) => { + const activeListenerBreakpoints = await getActiveEventListeners(getState()); + + const newEvents = remove( + activeListenerBreakpoints, + event => !eventsToRemove.includes(event) + ); + + updateBreakpoints(dispatch, client, newEvents); + }; +} + +export function addEventListenerExpanded(category: string) { + return async ({ dispatch, getState }: ThunkArgs) => { + const expanded = await getEventListenerExpanded(getState()); + + const newExpanded = uniq([...expanded, category]); + + await updateExpanded(dispatch, newExpanded); + }; +} + +export function removeEventListenerExpanded(category: string) { + return async ({ dispatch, getState }: ThunkArgs) => { + const expanded = await getEventListenerExpanded(getState()); + + const newExpanded = expanded.filter(expand => expand != category); + + updateExpanded(dispatch, newExpanded); + }; +} + +export function getEventListenerBreakpointTypes() { + return async ({ dispatch, client }: ThunkArgs) => { + const categories = await client.getEventListenerBreakpointTypes(); + dispatch({ type: "RECEIVE_EVENT_LISTENER_TYPES", categories }); }; } diff --git a/devtools/client/debugger/src/actions/types/index.js b/devtools/client/debugger/src/actions/types/index.js index 51a110cf42c6..01296b64b8e7 100644 --- a/devtools/client/debugger/src/actions/types/index.js +++ b/devtools/client/debugger/src/actions/types/index.js @@ -159,6 +159,30 @@ export type { export type { panelPositionType } from "./UIAction"; +export type { ASTAction } from "./ASTAction"; + +type ActiveEventListener = string; +type EventListenerEvent = { name: string, id: ActiveEventListener }; +type EventListenerCategory = { name: string, events: EventListenerEvent[] }; + +export type EventListenerActiveList = ActiveEventListener[]; +export type EventListenerCategoryList = EventListenerCategory[]; +export type EventListenerExpandedList = string[]; + +export type EventListenerAction = + | {| + +type: "UPDATE_EVENT_LISTENERS", + +active: EventListenerActiveList, + |} + | {| + +type: "RECEIVE_EVENT_LISTENER_TYPES", + +categories: EventListenerCategoryList, + |} + | {| + +type: "UPDATE_EVENT_LISTENER_EXPANDED", + +expanded: EventListenerExpandedList, + |}; + /** * Actions: Source, Breakpoint, and Navigation * @@ -180,4 +204,5 @@ export type Action = | FileTextSearchAction | ProjectTextSearchAction | DebuggeeAction - | SourceTreeAction; + | SourceTreeAction + | EventListenerAction; diff --git a/devtools/client/debugger/src/client/firefox.js b/devtools/client/debugger/src/client/firefox.js index 3d8fb32624cb..e24f96eb7bee 100644 --- a/devtools/client/debugger/src/client/firefox.js +++ b/devtools/client/debugger/src/client/firefox.js @@ -47,6 +47,9 @@ export async function onConnect(connection: any, actions: Object) { skipBreakpoints: prefs.skipPausing, }); + // Retrieve possible event listener breakpoints + actions.getEventListenerBreakpointTypes(); + // In Firefox, we need to initially request all of the sources. This // usually fires off individual `newSource` notifications as the // debugger finds them, but there may be existing sources already in diff --git a/devtools/client/debugger/src/client/firefox/commands.js b/devtools/client/debugger/src/client/firefox/commands.js index 59758b83ee75..b7cf035a7f43 100644 --- a/devtools/client/debugger/src/client/firefox/commands.js +++ b/devtools/client/debugger/src/client/firefox/commands.js @@ -16,7 +16,6 @@ import type { BreakpointLocation, BreakpointOptions, PendingLocation, - EventListenerBreakpoints, Frame, FrameId, GeneratedSourceData, @@ -36,6 +35,8 @@ import type { SourcesPacket, } from "./types"; +import type { EventListenerCategoryList } from "../../actions/types"; + let workerClients: Object; let threadClient: ThreadClient; let tabTarget: TabTarget; @@ -361,8 +362,17 @@ function interrupt(thread: string): Promise<*> { return lookupThreadClient(thread).interrupt(); } -function setEventListenerBreakpoints(eventTypes: EventListenerBreakpoints) { - // TODO: Figure out what sendpoint we want to hit +async function setEventListenerBreakpoints(ids: string[]) { + await threadClient.setActiveEventBreakpoints(ids); + await forEachWorkerThread(thread => thread.setActiveEventBreakpoints(ids)); +} + +// eslint-disable-next-line +async function getEventListenerBreakpointTypes(): Promise< + EventListenerCategoryList +> { + const { value } = await threadClient.getAvailableEventBreakpoints(); + return value; } function pauseGrip(thread: string, func: Function): ObjectClient { @@ -525,6 +535,7 @@ const clientCommands = { sendPacket, setSkipPausing, setEventListenerBreakpoints, + getEventListenerBreakpointTypes, waitForWorkers, detachWorkers, hasWasmSupport, diff --git a/devtools/client/debugger/src/client/firefox/types.js b/devtools/client/debugger/src/client/firefox/types.js index 8f87962d777c..3c89c5041aec 100644 --- a/devtools/client/debugger/src/client/firefox/types.js +++ b/devtools/client/debugger/src/client/firefox/types.js @@ -25,6 +25,8 @@ import type { Range, } from "../../types"; +import type { EventListenerCategoryList } from "../../actions/types"; + type URL = string; /** @@ -369,7 +371,10 @@ export type ThreadClient = { actor: ActorId, request: (payload: Object) => Promise<*>, url: string, - setEventListenerBreakpoints: (string[]) => void, + setActiveEventBreakpoints: (string[]) => void, + getAvailableEventBreakpoints: () => Promise<{| + value: EventListenerCategoryList, + |}>, skipBreakpoints: boolean => Promise<{| skip: boolean |}>, }; diff --git a/devtools/client/debugger/src/client/index.js b/devtools/client/debugger/src/client/index.js index e68801c9ec6f..e4fe19c14671 100644 --- a/devtools/client/debugger/src/client/index.js +++ b/devtools/client/debugger/src/client/index.js @@ -15,6 +15,7 @@ import { bootstrapStore, bootstrapWorkers, } from "../utils/bootstrap"; + import { initialBreakpointsState } from "../reducers/breakpoints"; import type { Panel } from "./firefox/types"; @@ -47,7 +48,12 @@ async function loadInitialState() { const breakpoints = initialBreakpointsState(xhrBreakpoints); - return { pendingBreakpoints, tabs, breakpoints, eventListenerBreakpoints }; + return { + pendingBreakpoints, + tabs, + breakpoints, + eventListenerBreakpoints, + }; } function getClient(connection: any) { diff --git a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js index be2ce126448f..ad18d6c0a01d 100644 --- a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js +++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js @@ -9,141 +9,121 @@ import classnames from "classnames"; import { connect } from "../../utils/connect"; import actions from "../../actions"; -import { getActiveEventListeners } from "../../selectors"; +import { + getActiveEventListeners, + getEventListenerBreakpointTypes, + getEventListenerExpanded, +} from "../../selectors"; import AccessibleImage from "../shared/AccessibleImage"; -import type { EventListenerBreakpoints } from "../../types"; +import type { + EventListenerActiveList, + EventListenerCategoryList, + EventListenerExpandedList, +} from "../../actions/types"; import "./EventListeners.css"; -const CATEGORIES = { - Mouse: ["click", "mouseover", "dblclick"], - Keyboard: ["keyup", "keydown"], -}; - type Props = { - addEventListeners: typeof actions.addEventListeners, - removeEventListeners: typeof actions.removeEventListeners, - activeEventListeners: EventListenerBreakpoints, + categories: EventListenerCategoryList, + expandedCategories: EventListenerExpandedList, + activeEventListeners: EventListenerActiveList, + addEventListeners: typeof actions.addEventListenerBreakpoints, + removeEventListeners: typeof actions.removeEventListenerBreakpoints, + addEventListenerExpanded: typeof actions.addEventListenerExpanded, + removeEventListenerExpanded: typeof actions.removeEventListenerExpanded, }; -type State = { - expandedCategories: string[], -}; - -function getKey(category: string, eventType: string) { - return `${category}:${eventType}`; -} - -class EventListeners extends Component { - constructor(props) { - super(props); - - this.state = { - expandedCategories: [], - }; - } - - onCategoryToggle(category, event) { - const { expandedCategories } = this.state; +class EventListeners extends Component { + onCategoryToggle(category) { + const { + expandedCategories, + removeEventListenerExpanded, + addEventListenerExpanded, + } = this.props; if (expandedCategories.includes(category)) { - this.setState({ - expandedCategories: expandedCategories.filter( - eventCategory => eventCategory !== category - ), - }); + removeEventListenerExpanded(category); } else { - this.setState({ - expandedCategories: [...expandedCategories, category], - }); + addEventListenerExpanded(category); } } onCategoryClick(category, isChecked) { const { addEventListeners, removeEventListeners } = this.props; - const events = CATEGORIES[category].map(eventType => - getKey(category, eventType) - ); + const eventsIds = category.events.map(event => event.id); if (isChecked) { - addEventListeners(events); + addEventListeners(eventsIds); } else { - removeEventListeners(events); + removeEventListeners(eventsIds); } } - onEventTypeClick(eventType, isChecked) { + onEventTypeClick(eventId, isChecked) { const { addEventListeners, removeEventListeners } = this.props; if (isChecked) { - addEventListeners([eventType]); + addEventListeners([eventId]); } else { - removeEventListeners([eventType]); + removeEventListeners([eventId]); } } renderCategoryHeading(category) { - const { expandedCategories } = this.state; - const { activeEventListeners } = this.props; + const { activeEventListeners, expandedCategories } = this.props; + const { events } = category; - const eventTypes = CATEGORIES[category]; - - const expanded = expandedCategories.includes(category); - const checked = eventTypes.every(eventType => - activeEventListeners.includes(getKey(category, eventType)) - ); + const expanded = expandedCategories.includes(category.name); + const checked = events.every(({ id }) => activeEventListeners.includes(id)); const indeterminate = - !checked && - eventTypes.some(eventType => - activeEventListeners.includes(getKey(category, eventType)) - ); + !checked && events.some(({ id }) => activeEventListeners.includes(id)); return (
); } renderCategoryListing(category) { - const { activeEventListeners } = this.props; - const { expandedCategories } = this.state; + const { activeEventListeners, expandedCategories } = this.props; - const expanded = expandedCategories.includes(category); + const expanded = expandedCategories.includes(category.name); if (!expanded) { return null; } return (