Skip to content

Commit

Permalink
feat: add tabs to switch between query and event mode (#296)
Browse files Browse the repository at this point in the history
  • Loading branch information
aganglada committed Dec 3, 2020
1 parent 28d506d commit 351c6ff
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 258 deletions.
19 changes: 10 additions & 9 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Router>
<Switch>
<Route path="/embed/:gistId?/:gistVersion?" component={Embedded} />
<Route path="/events" component={DomEvents} />
<Route
path={['/gist/:gistId/:gistVersion?', '/']}
component={Playground}
/>
</Switch>
<PreviewEventsProvider>
<Switch>
<Route path="/embed/:gistId?/:gistVersion?" component={Embedded} />
<Route
path={['/gist/:gistId/:gistVersion?', '/']}
component={Playground}
/>
</Switch>
</PreviewEventsProvider>
</Router>
);
}
Expand Down
260 changes: 54 additions & 206 deletions src/components/DomEvents.js
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -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 = () => (
<IconButton>
Expand All @@ -126,143 +50,67 @@ function DomEvents() {
</IconButton>
);

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 (
<Layout
dispatch={dispatch}
gistId={gistId}
dirty={dirty}
status={status}
settings={settings}
>
<div className="flex flex-col h-auto md:h-full w-full">
<div className="editor p-4 markup-editor gap-4 md:gap-8 md:h-56 flex-auto grid-cols-1 md:grid-cols-2">
<div className="flex-auto relative h-56 md:h-full">
<MarkupEditor markup={markup} dispatch={dispatch} />
<div className="editor p-4 h-56 flex-auto overflow-hidden">
<div className="h-full w-full flex flex-col">
<div className="h-8 flex items-center w-full text-sm font-bold">
<div
className="p-2 w-16 cursor-pointer flex justify-between items-center"
onClick={changeSortDirection}
>
# {getSortIcon()}
</div>

<div className="flex-auto h-56 md:h-full">
<Preview
forwardedRef={setPreviewRef}
markup={markup}
elements={result?.elements}
accessibleRoles={result?.accessibleRoles}
dispatch={dispatch}
variant="minimal"
/>
<div className="p-2 w-32 ">type</div>
<div className="p-2 w-32 ">name</div>

<div className="p-2 w-40 ">element</div>
<div className="flex-auto p-2 flex justify-between">
<span>selector</span>
<div>
<CopyButton
text={getTextToCopy}
title="copy log"
className="mr-5"
/>
<IconButton title="clear event log" onClick={reset}>
<TrashcanIcon />
</IconButton>
</div>
</div>
</div>

<div className="flex-none h-8" />

<div className="editor p-4 md:h-56 flex-auto overflow-hidden">
<div className="h-56 md:h-full w-full flex flex-col">
<div className="h-8 flex items-center w-full text-sm font-bold">
<div
className="p-2 w-16 cursor-pointer flex justify-between items-center"
onClick={changeSortDirection}
>
# {getSortIcon()}
</div>

<div className="p-2 w-32 ">type</div>
<div className="p-2 w-32 ">name</div>

<div className="p-2 w-40 ">element</div>
<div className="flex-auto p-2 flex justify-between">
<span>selector</span>
<div>
<CopyButton
text={getTextToCopy}
title="copy log"
className="mr-5"
/>
<IconButton title="clear event log" onClick={reset}>
<TrashcanIcon />
</IconButton>
</div>
</div>
<div className="flex-auto relative overflow-hidden">
{buffer.current.length === 0 ? (
<div className="flex w-full h-full opacity-50 items-end justify-center">
<EmptyStreetImg height="80%" />
</div>

<div className="flex-auto relative overflow-hidden">
{buffer.current.length === 0 ? (
<div className="flex w-full h-full opacity-50 items-end justify-center">
<EmptyStreetImg height="80%" />
</div>
) : (
<AutoSizer>
{({ width, height }) => (
<StickyList
mode={appendMode}
ref={listRef}
height={height}
itemCount={eventCount}
itemData={buffer.current}
itemSize={32}
width={width}
outerElementType={VirtualScrollable}
>
{EventRecord}
</StickyList>
)}
</AutoSizer>
) : (
<AutoSizer>
{({ width, height }) => (
<StickyList
mode={appendMode}
ref={listRef}
height={height}
itemCount={eventCount}
itemData={buffer.current}
itemSize={32}
width={width}
outerElementType={VirtualScrollable}
>
{EventRecord}
</StickyList>
)}
</div>
</div>
</AutoSizer>
)}
</div>
</div>
</Layout>
</div>
);
}

Expand Down
19 changes: 1 addition & 18 deletions src/components/Embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<button
disabled={disabled}
className={[
'text-xs select-none border-b-2',
disabled ? '' : 'hover:text-blue-400 hover:border-blue-400',
active
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-800',
].join(' ')}
onClick={disabled ? undefined : onClick}
>
{children}
</button>
);
}
import TabButton from './TabButton';

const possiblePanes = ['markup', 'preview', 'query', 'result'];

Expand Down
2 changes: 1 addition & 1 deletion src/components/Loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function Loader({ loading }) {
<div
className={[
'w-full h-full absolute top-0 left-0 flex flex-col justify-center items-center w-full h-full space-y-4 fade',
loading ? 'opacity-100' : 'opacity-0',
loading ? 'opacity-100' : 'hidden opacity-0',
].join(' ')}
>
<img className="opacity-50" src={frog} />
Expand Down

0 comments on commit 351c6ff

Please sign in to comment.