Added copy and download to output. Improved tooltip component

This commit is contained in:
Maksym Bykovskyy 2020-02-15 17:49:46 -08:00
parent 26332f396e
commit ddd148a290
8 changed files with 244 additions and 68 deletions

View File

@ -7,6 +7,7 @@ import { Examples } from './components/examples';
import { FloatButtonComponent } from './components/float-button';
import { HeaderComponent } from './components/header';
import { TabsPanelComponent } from './components/tabs-panel';
import { TooltipContainer } from './components/tooltip';
import configureStore from './configure-store';
const store = configureStore();
@ -48,6 +49,7 @@ const App: React.FC = () => {
href="https://discord.gg/9rhYaEt"
></FloatButtonComponent>
</FeedbackContainer>
<TooltipContainer></TooltipContainer>
</Provider>
);
};

View File

@ -1,5 +1,7 @@
import React, { useState } from 'react';
import styled, { css } from 'styled-components';
import React from 'react';
import styled from 'styled-components';
import { Tooltip } from './tooltip';
const Container = styled.div`
display: flex;
@ -36,46 +38,16 @@ const Button = styled.a`
}
`;
const Tooltip = styled.div<{ visible?: boolean }>`
position: absolute;
pointer-events: none;
z-index: 3;
white-space: nowrap;
transform: translateX(-6.5em);
font-size: var(--font_sub_size);
color: var(--tooltip_foreground);
background-color: var(--tooltip_background);
border-radius: 6px;
padding: 5px 10px;
opacity: 0;
transition: opacity 0.2s ease 0.2s;
${props =>
props.visible &&
css`
opacity: 1;
`}
`;
export const FloatButtonComponent = (props: {
tooltip: string;
text: string;
href: string;
className?: string;
}) => {
const [isTooltipShowing, setShowTooltip] = useState(false);
return (
<Container className={props.className}>
<Tooltip visible={isTooltipShowing}>{props.tooltip}</Tooltip>
<Button
onMouseOver={() => setShowTooltip(true)}
onMouseOut={() => setShowTooltip(false)}
href={props.href}
target="_blank"
rel="noopener noreferrer"
>
<Tooltip position="left">{props.tooltip}</Tooltip>
<Button href={props.href} target="_blank" rel="noopener noreferrer">
{props.text}
</Button>
</Container>

View File

@ -7,6 +7,7 @@ import { AppState } from '../redux/app';
import { CommandState } from '../redux/command';
import { DoneLoadingAction, LoadingState } from '../redux/loading';
import { ResultState } from '../redux/result';
import { OutputToolbarComponent } from './output-toolbar';
const Container = styled.div<{ visible?: boolean }>`
position: absolute;
@ -15,8 +16,8 @@ const Container = styled.div<{ visible?: boolean }>`
height: 100%;
font-family: Menlo, Monaco, 'Courier New', monospace;
overflow: scroll;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.2s ease-in;
@ -42,9 +43,9 @@ const CancelButton = styled.div`
const Output = styled.div`
flex: 1;
padding: 0.8em;
padding: 0 0.5em 0.5em 0.5em;
display: flex;
overflow: scroll;
/* This font size is used to calcuate spinner size */
font-size: 1em;
`;
@ -65,6 +66,37 @@ const Pre = styled.pre`
margin: 0;
`;
function copyOutput(el: HTMLElement | null) {
if (el) {
const range = document.createRange();
range.selectNodeContents(el);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
}
}
}
function downloadOutput(el: HTMLElement | null) {
if (el) {
const anchor = document.createElement('a');
anchor.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(el.innerHTML)
);
anchor.setAttribute('download', 'output.txt');
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
}
export const OutputTabComponent = (props: {
selected?: boolean;
onCancel?: () => void;
@ -85,13 +117,14 @@ export const OutputTabComponent = (props: {
const dispatch = useDispatch();
const outputRef = useRef(null);
const outputRef = useRef<HTMLDivElement>(null);
const preRef = useRef<HTMLPreElement>(null);
const [spinnerSize, setSpinnerSize] = useState(50);
useEffect(() => {
const htmlElement = (outputRef.current as unknown) as HTMLElement;
const outputEl = (outputRef.current as unknown) as HTMLElement;
const fontSize = window
.getComputedStyle(htmlElement, null)
.getComputedStyle(outputEl, null)
.getPropertyValue('font-size');
setSpinnerSize(parseFloat(fontSize) * 3);
@ -99,6 +132,12 @@ export const OutputTabComponent = (props: {
return (
<Container visible={props.selected}>
{output.length !== 0 && (
<OutputToolbarComponent
onCopy={() => copyOutput(preRef.current)}
onDownload={() => downloadOutput(preRef.current)}
></OutputToolbarComponent>
)}
<Output id="output" ref={outputRef}>
{loading.loading && (
<LoadingContainer>
@ -122,7 +161,7 @@ export const OutputTabComponent = (props: {
</LoadingContainer>
)}
{!loading.loading &&
((output.length !== 0 && <Pre>{output}</Pre>) ||
((output.length !== 0 && <Pre ref={preRef}>{output}</Pre>) ||
(contract.length !== 0 && (
<span>
The contract was successfully deployed to the babylonnet test

View File

@ -0,0 +1,78 @@
import { faCopy, faDownload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import styled from 'styled-components';
import { Tooltip } from './tooltip';
const Container = styled.div`
display: flex;
justify-content: flex-start;
padding: 0.2em 0.5em;
z-index: 3;
`;
const Action = styled.div`
z-index: 3;
position: relative;
margin: 4px 6px;
cursor: pointer;
opacity: 0.5;
color: #444;
::before {
content: '';
display: block;
position: absolute;
z-index: -1;
bottom: -4px;
left: -4px;
right: -4px;
top: -4px;
border-radius: 4px;
background: none;
box-sizing: border-box;
opacity: 0;
transform: scale(0);
transition-property: transform, opacity;
transition-duration: 0.15s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
:hover::before {
background-color: rgba(32, 33, 36, 0.059);
opacity: 1;
transform: scale(1);
}
:hover {
opacity: 1;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
`;
export const OutputToolbarComponent = (props: {
onCopy?: () => void;
onDownload?: () => void;
}) => {
return (
<Container>
<Action onClick={() => props.onCopy && props.onCopy()}>
<FontAwesomeIcon icon={faCopy}></FontAwesomeIcon>
<Tooltip>Copy</Tooltip>
</Action>
<Action onClick={() => props.onDownload && props.onDownload()}>
<FontAwesomeIcon icon={faDownload}></FontAwesomeIcon>
<Tooltip>Download</Tooltip>
</Action>
</Container>
);
};

View File

@ -8,6 +8,7 @@ import styled, { css } from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeShareLinkAction, ShareState } from '../redux/share';
import { share } from '../services/api';
import { Tooltip } from './tooltip';
const Container = styled.div`
display: flex;
@ -96,26 +97,6 @@ const Input = styled.input<{ visible?: boolean }>`
`}
`;
const Tooltip = styled.div<{ visible?: boolean }>`
position: absolute;
pointer-events: none;
z-index: 3;
transform: translateY(2.5em);
font-size: var(--font_sub_size);
color: var(--tooltip_foreground);
background-color: var(--tooltip_background);
border-radius: 6px;
padding: 5px 10px;
opacity: 0;
transition: opacity 0.2s ease 0.2s;
${props =>
props.visible &&
css`
opacity: 1;
`}
`;
const shareAction = () => {
return async function(dispatch: Dispatch, getState: () => AppState) {
try {
@ -138,7 +119,6 @@ export const ShareComponent = () => {
state => state.share.link
);
const [clicked, setClicked] = useState(false);
const [isTooltipShowing, setShowTooltip] = useState(false);
const SHARE_TOOLTIP = 'Share code';
const COPY_TOOLTIP = 'Copy link';
@ -149,14 +129,12 @@ export const ShareComponent = () => {
if (shareLink) {
if (inputEl.current && copy(inputEl.current)) {
setTooltipMessage(COPIED_TOOLTIP);
setShowTooltip(true);
} else {
setClicked(true);
setTooltipMessage(COPY_TOOLTIP);
}
} else {
setClicked(false);
setShowTooltip(false);
setTooltipMessage(SHARE_TOOLTIP);
}
}, [shareLink]);
@ -177,9 +155,7 @@ export const ShareComponent = () => {
if (tooltipMessage === COPIED_TOOLTIP) {
setTooltipMessage(COPY_TOOLTIP);
}
setShowTooltip(true);
}}
onMouseOut={() => setShowTooltip(false)}
onClick={() => {
if (!shareLink) {
dispatch(shareAction());
@ -193,7 +169,7 @@ export const ShareComponent = () => {
>
<Label visible={!clicked}>Share</Label>
<Copy visible={clicked}></Copy>
<Tooltip visible={isTooltipShowing}>{tooltipMessage}</Tooltip>
<Tooltip>{tooltipMessage}</Tooltip>
</Button>
</Container>
);

View File

@ -69,7 +69,11 @@ export const TabsPanelComponent = () => {
<Container>
<Header>
{TABS.map(tab => (
<Tab id={tab.id} selected={selectedTab.index === tab.index}>
<Tab
key={tab.id}
id={tab.id}
selected={selectedTab.index === tab.index}
>
<Label onClick={() => selectTab(tab)}>{tab.label}</Label>
</Tab>
))}

View File

@ -0,0 +1,104 @@
import React, { createElement, useEffect, useRef, useState } from 'react';
import { render } from 'react-dom';
import styled from 'styled-components';
const Container = styled.div`
position: fixed;
z-index: 1000;
top: 0;
left: 0;
height: 100%;
width: 100%;
pointer-events: none;
`;
export const StyledTooltip = styled.div<{
visible: boolean;
x: string;
y: string;
}>`
position: fixed;
pointer-events: none;
z-index: 1001;
font-size: var(--font_sub_size);
color: var(--tooltip_foreground);
background-color: var(--tooltip_background);
border-radius: 6px;
padding: 5px 10px;
opacity: 0;
transition: opacity 0.2s ease 0.2s;
transform-origin: center;
${({ x, y }) => `transform: translate(calc(${x}), calc(${y}));`}
${({ visible }) => visible && `opacity: 1;`}
`;
const TOOLTIP_CONTAINER_ID = 'tooltip-container';
type Position = 'top' | 'bottom' | 'left' | 'right';
export const TooltipContainer = () => {
return <Container id={TOOLTIP_CONTAINER_ID}></Container>;
};
function calcX(triggerRect: ClientRect, position?: Position) {
if ('left' === position) {
return `${triggerRect.left - 10}px - 100%`;
} else if ('right' === position) {
return `${triggerRect.right + 10}px`;
}
return `${triggerRect.left + triggerRect.width / 2}px - 50%`;
}
function calcY(triggerRect: ClientRect, position?: string) {
if ('top' === position) {
return `${triggerRect.top - 10}px - 100%`;
} else if (!position || 'bottom' === position) {
return `${triggerRect.bottom + 10}px`;
}
return `${triggerRect.top + triggerRect.height / 2}px - 50%`;
}
export const Tooltip = (props: { position?: Position; children: any }) => {
const ref = useRef<HTMLDivElement>(null);
const [isTooltipVisible, setTooltipVisible] = useState(false);
const renderTooltip = (visible: boolean, triggerRect: ClientRect) => {
const tooltip = createElement(
StyledTooltip,
{
visible,
x: calcX(triggerRect, props.position),
y: calcY(triggerRect, props.position)
},
props.children
);
render(tooltip, document.getElementById(TOOLTIP_CONTAINER_ID));
};
useEffect(() => {
if (ref.current) {
const trigger = (ref.current as HTMLElement).parentElement;
if (trigger) {
if (isTooltipVisible) {
renderTooltip(true, trigger.getBoundingClientRect());
}
trigger.onmouseenter = _ => {
renderTooltip(true, trigger.getBoundingClientRect());
setTooltipVisible(true);
};
trigger.onmouseleave = _ => {
renderTooltip(false, trigger.getBoundingClientRect());
setTooltipVisible(false);
};
}
}
});
return <div ref={ref}></div>;
};

View File

@ -50,6 +50,7 @@
--tooltip_foreground: white;
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;
--label_foreground: rgba(153, 153, 153, 1);
}
body {