From d3e1044a4244863f4069707df9e11ddea8eb5408 Mon Sep 17 00:00:00 2001 From: Alejandro Garcia Anglada Date: Fri, 13 Nov 2020 12:49:52 +0000 Subject: [PATCH] Move Events to Tabs --- src/components/App.js | 19 ++- src/components/DomEvents.js | 260 ++++++----------------------- src/components/Embed.js | 19 +-- src/components/Loader.js | 2 +- src/components/Playground.js | 33 +--- src/components/PlaygroundPanels.js | 63 +++++++ src/components/TabButton.js | 21 +++ src/context/PreviewEvents.js | 113 +++++++++++++ src/lib/domEvents.js | 65 ++++++++ 9 files changed, 337 insertions(+), 258 deletions(-) create mode 100644 src/components/PlaygroundPanels.js create mode 100644 src/components/TabButton.js create mode 100644 src/context/PreviewEvents.js create mode 100644 src/lib/domEvents.js diff --git a/src/components/App.js b/src/components/App.js index 9b4162a3..65e58a8b 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -2,19 +2,20 @@ import React from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import Playground from './Playground'; import Embedded from './Embedded'; -import DomEvents from './DomEvents'; +import { PreviewEventsProvider } from '../context/PreviewEvents'; function App() { return ( - - - - - + + + + + + ); } diff --git a/src/components/DomEvents.js b/src/components/DomEvents.js index 487e9530..7e953ee4 100644 --- a/src/components/DomEvents.js +++ b/src/components/DomEvents.js @@ -1,84 +1,18 @@ -import React, { useRef, useCallback, useState } from 'react'; -import { eventMap } from '@testing-library/dom/dist/event-map'; +import React, { useRef } from 'react'; import { ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react'; -import throttle from 'lodash.throttle'; + import AutoSizer from 'react-virtualized-auto-sizer'; import { TrashcanIcon } from '@primer/octicons-react'; -import Preview from './Preview'; -import MarkupEditor from './MarkupEditor'; -import usePlayground from '../hooks/usePlayground'; import { VirtualScrollable } from './Scrollable'; import IconButton from './IconButton'; import CopyButton from './CopyButton'; import EmptyStreetImg from '../images/EmptyStreetImg'; import StickyList from './StickyList'; -import Layout from './Layout'; -import { useParams } from 'react-router-dom'; - -function targetToString() { - return [ - this.tagName.toLowerCase(), - this.id && `#${this.id}`, - this.name && `[name="${this.name}"]`, - this.htmlFor && `[for="${this.htmlFor}"]`, - this.value && `[value="${this.value}"]`, - this.checked !== null && `[checked=${this.checked}]`, - ] - .filter(Boolean) - .join(''); -} - -function getElementData(element) { - const value = - element.tagName === 'SELECT' && element.multiple - ? element.selectedOptions.length > 0 - ? JSON.stringify( - Array.from(element.selectedOptions).map((o) => o.value), - ) - : null - : element.value; - - const hasChecked = element.type === 'checkbox' || element.type === 'radio'; - - return { - tagName: element.tagName.toLowerCase(), - id: element.id || null, - name: element.name || null, - htmlFor: element.htmlFor || null, - value: value || null, - checked: hasChecked ? !!element.checked : null, - toString: targetToString, - }; -} - -function addLoggingEvents(node, log) { - function createEventLogger(eventType) { - return function logEvent(event) { - if (event.target === event.currentTarget) { - return; - } - - log({ - event: eventType, - target: getElementData(event.target), - }); - }; - } - const eventListeners = []; - Object.keys(eventMap).forEach((name) => { - eventListeners.push({ - name: name.toLowerCase(), - listener: node.addEventListener( - name.toLowerCase(), - createEventLogger({ name, ...eventMap[name] }), - true, - ), - }); - }); - - return eventListeners; -} +import { + usePreviewEvents, + usePreviewEventsActions, +} from '../context/PreviewEvents'; function EventRecord({ index, style, data }) { const { id, type, name, element, selector } = data[index]; @@ -102,19 +36,9 @@ function EventRecord({ index, style, data }) { } function DomEvents() { - const { gistId, gistVersion } = useParams(); - - const buffer = useRef([]); - const previewRef = useRef(); const listRef = useRef(); - - const sortDirection = useRef('asc'); - const [appendMode, setAppendMode] = useState('bottom'); - const [state, dispatch] = usePlayground({ gistId, gistVersion }); - const { markup, result, status, dirty, settings } = state; - - const [eventCount, setEventCount] = useState(0); - const [eventListeners, setEventListeners] = useState([]); + const { sortDirection, buffer, appendMode, eventCount } = usePreviewEvents(); + const { changeSortDirection, reset } = usePreviewEventsActions(); const getSortIcon = () => ( @@ -126,143 +50,67 @@ function DomEvents() { ); - const changeSortDirection = () => { - const newDirection = sortDirection.current === 'desc' ? 'asc' : 'desc'; - buffer.current = buffer.current.reverse(); - setAppendMode(newDirection === 'desc' ? 'top' : 'bottom'); - sortDirection.current = newDirection; - }; - - const reset = () => { - buffer.current = []; - setEventCount(0); - }; - const getTextToCopy = () => buffer.current .map((log) => `${log.target.toString()} - ${log.event.EventType}`) .join('\n'); - const flush = useCallback( - throttle(() => setEventCount(buffer.current.length), 16, { - leading: false, - }), - [setEventCount], - ); - - const setPreviewRef = useCallback((node) => { - if (node) { - previewRef.current = node; - const eventListeners = addLoggingEvents(node, (event) => { - const log = { - id: buffer.current.length + 1, - type: event.event.EventType, - name: event.event.name, - element: event.target.tagName, - selector: event.target.toString(), - }; - if (sortDirection.current === 'desc') { - buffer.current.splice(0, 0, log); - } else { - buffer.current.push(log); - } - - setTimeout(flush, 0); - }); - setEventListeners(eventListeners); - } else if (previewRef.current) { - eventListeners.forEach((event) => - previewRef.current.removeEventListener(event.name, event.listener), - ); - previewRef.current = null; - } - }, []); - return ( - -
-
-
- +
+
+
+
+ # {getSortIcon()}
-
- +
type
+
name
+ +
element
+
+ selector +
+ + + + +
-
- -
-
-
-
- # {getSortIcon()} -
- -
type
-
name
- -
element
-
- selector -
- - - - -
-
+
+ {buffer.current.length === 0 ? ( +
+
- -
- {buffer.current.length === 0 ? ( -
- -
- ) : ( - - {({ width, height }) => ( - - {EventRecord} - - )} - + ) : ( + + {({ width, height }) => ( + + {EventRecord} + )} -
-
+ + )}
- +
); } diff --git a/src/components/Embed.js b/src/components/Embed.js index 58d0fd4c..d699adf6 100644 --- a/src/components/Embed.js +++ b/src/components/Embed.js @@ -5,24 +5,7 @@ import Embedded from './Embedded'; import { SyncIcon, XIcon } from '@primer/octicons-react'; import { defaultPanes } from '../constants'; - -function TabButton({ children, active, onClick, disabled }) { - return ( - - ); -} +import TabButton from './TabButton'; const possiblePanes = ['markup', 'preview', 'query', 'result']; diff --git a/src/components/Loader.js b/src/components/Loader.js index 3e1e6f3a..6369949c 100644 --- a/src/components/Loader.js +++ b/src/components/Loader.js @@ -6,7 +6,7 @@ function Loader({ loading }) {
diff --git a/src/components/Playground.js b/src/components/Playground.js index d5cb7f26..1759b352 100644 --- a/src/components/Playground.js +++ b/src/components/Playground.js @@ -2,24 +2,17 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import Preview from './Preview'; import MarkupEditor from './MarkupEditor'; -import Result from './Result'; -import Query from './Query'; import usePlayground from '../hooks/usePlayground'; import Layout from './Layout'; import Loader from './Loader'; - -function Paper({ children }) { - return ( -
- {children} -
- ); -} +import PlaygroundPanels from './PlaygroundPanels'; +import { usePreviewEvents } from '../context/PreviewEvents'; function Playground() { const { gistId, gistVersion } = useParams(); const [state, dispatch] = usePlayground({ gistId, gistVersion }); - const { markup, query, result, status, dirty, settings } = state; + const { markup, result, status, dirty, settings } = state; + const { previewRef } = usePreviewEvents(); const isLoading = status === 'loading'; @@ -39,32 +32,24 @@ function Playground() { isLoading ? 'opacity-0' : 'opacity-100', ].join(' ')} > - +
- - -
+
- -
- -
- -
- -
-
+
+
); diff --git a/src/components/PlaygroundPanels.js b/src/components/PlaygroundPanels.js new file mode 100644 index 00000000..c3bf9ac5 --- /dev/null +++ b/src/components/PlaygroundPanels.js @@ -0,0 +1,63 @@ +import React, { Suspense } from 'react'; +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import usePlayground from '../hooks/usePlayground'; + +import Query from './Query'; +import Result from './Result'; +import TabButton from './TabButton'; + +const panels = ['Query', 'Events']; + +const DomEvents = React.lazy(() => import('./DomEvents')); + +function Paper({ children }) { + return ( +
+ {children} +
+ ); +} + +function PlaygroundPanels() { + const { gistId, gistVersion } = useParams(); + const [state, dispatch] = usePlayground({ gistId, gistVersion }); + const { query, result } = state; + const [panel, setPanel] = useState(panels[0]); + + return ( + <> +
+ {panels.map((panelName) => ( +
+
+ setPanel(panelName)} + active={panelName === panel} + > + {panelName} + +
+
+ ))} +
+ + {panel === panels[0] && ( + +
+ +
+ +
+ +
+
+ )} + {panel === panels[1] && } +
+ + ); +} + +export default PlaygroundPanels; diff --git a/src/components/TabButton.js b/src/components/TabButton.js new file mode 100644 index 00000000..04d52d3f --- /dev/null +++ b/src/components/TabButton.js @@ -0,0 +1,21 @@ +import React from 'react'; + +function TabButton({ children, active, onClick, disabled }) { + return ( + + ); +} + +export default TabButton; diff --git a/src/context/PreviewEvents.js b/src/context/PreviewEvents.js new file mode 100644 index 00000000..0ca7ffc6 --- /dev/null +++ b/src/context/PreviewEvents.js @@ -0,0 +1,113 @@ +import React, { + useCallback, + useRef, + useState, + useContext, + createContext, +} from 'react'; +import throttle from 'lodash.throttle'; + +import { addLoggingEvents } from '../lib/domEvents'; + +const PreviewEventsContext = createContext(); +const PreviewEventsActionsContext = createContext(); + +export function PreviewEventsProvider({ children }) { + const buffer = useRef([]); + const previewRef = useRef(); + const sortDirection = useRef('asc'); + const [appendMode, setAppendMode] = useState('bottom'); + const [eventCount, setEventCount] = useState(0); + const [eventListeners, setEventListeners] = useState([]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const flush = useCallback( + throttle(() => setEventCount(buffer.current.length), 16, { + leading: false, + }), + [setEventCount], + ); + + const changeSortDirection = () => { + const newDirection = sortDirection.current === 'desc' ? 'asc' : 'desc'; + buffer.current = buffer.current.reverse(); + setAppendMode(newDirection === 'desc' ? 'top' : 'bottom'); + sortDirection.current = newDirection; + }; + + const reset = () => { + buffer.current = []; + setEventCount(0); + }; + + const setPreviewRef = useCallback((node) => { + if (node) { + previewRef.current = node; + const eventListeners = addLoggingEvents(node, (event) => { + const log = { + id: buffer.current.length + 1, + type: event.event.EventType, + name: event.event.name, + element: event.target.tagName, + selector: event.target.toString(), + }; + if (sortDirection.current === 'desc') { + buffer.current.splice(0, 0, log); + } else { + buffer.current.push(log); + } + + setTimeout(flush, 0); + }); + setEventListeners(eventListeners); + } else if (previewRef.current) { + eventListeners.forEach((event) => + previewRef.current.removeEventListener(event.name, event.listener), + ); + previewRef.current = null; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + {children} + + + ); +} + +export function usePreviewEvents() { + const context = useContext(PreviewEventsContext); + + if (!context) { + throw new Error( + `usePreviewEvents must be used within PreviewEventsProvider`, + ); + } + + return context; +} + +export function usePreviewEventsActions() { + const context = useContext(PreviewEventsActionsContext); + + if (!context) { + throw new Error( + `usePreviewEventsActions must be used within PreviewEventsProvider`, + ); + } + + return context; +} diff --git a/src/lib/domEvents.js b/src/lib/domEvents.js new file mode 100644 index 00000000..ac5b83e2 --- /dev/null +++ b/src/lib/domEvents.js @@ -0,0 +1,65 @@ +import { eventMap } from '@testing-library/dom/dist/event-map'; + +export function addLoggingEvents(node, log) { + function createEventLogger(eventType) { + return function logEvent(event) { + if (event.target === event.currentTarget) { + return; + } + + log({ + event: eventType, + target: getElementData(event.target), + }); + }; + } + const eventListeners = []; + Object.keys(eventMap).forEach((name) => { + eventListeners.push({ + name: name.toLowerCase(), + listener: node.addEventListener( + name.toLowerCase(), + createEventLogger({ name, ...eventMap[name] }), + true, + ), + }); + }); + + return eventListeners; +} + +export function getElementData(element) { + const value = + element.tagName === 'SELECT' && element.multiple + ? element.selectedOptions.length > 0 + ? JSON.stringify( + Array.from(element.selectedOptions).map((o) => o.value), + ) + : null + : element.value; + + const hasChecked = element.type === 'checkbox' || element.type === 'radio'; + + return { + tagName: element.tagName.toLowerCase(), + id: element.id || null, + name: element.name || null, + htmlFor: element.htmlFor || null, + value: value || null, + checked: hasChecked ? !!element.checked : null, + toString: targetToString, + }; +} + +export function targetToString() { + return [ + this.tagName.toLowerCase(), + this.id && `#${this.id}`, + this.name && `[name="${this.name}"]`, + this.htmlFor && `[for="${this.htmlFor}"]`, + this.value && `[value="${this.value}"]`, + this.checked !== null && `[checked=${this.checked}]`, + ] + .filter(Boolean) + .join(''); +}