Added copy and download to output. Improved tooltip component
This commit is contained in:
parent
26332f396e
commit
ddd148a290
@ -7,6 +7,7 @@ import { Examples } from './components/examples';
|
|||||||
import { FloatButtonComponent } from './components/float-button';
|
import { FloatButtonComponent } from './components/float-button';
|
||||||
import { HeaderComponent } from './components/header';
|
import { HeaderComponent } from './components/header';
|
||||||
import { TabsPanelComponent } from './components/tabs-panel';
|
import { TabsPanelComponent } from './components/tabs-panel';
|
||||||
|
import { TooltipContainer } from './components/tooltip';
|
||||||
import configureStore from './configure-store';
|
import configureStore from './configure-store';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
@ -48,6 +49,7 @@ const App: React.FC = () => {
|
|||||||
href="https://discord.gg/9rhYaEt"
|
href="https://discord.gg/9rhYaEt"
|
||||||
></FloatButtonComponent>
|
></FloatButtonComponent>
|
||||||
</FeedbackContainer>
|
</FeedbackContainer>
|
||||||
|
<TooltipContainer></TooltipContainer>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import styled, { css } from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Tooltip } from './tooltip';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
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: {
|
export const FloatButtonComponent = (props: {
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
text: string;
|
text: string;
|
||||||
href: string;
|
href: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [isTooltipShowing, setShowTooltip] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={props.className}>
|
<Container className={props.className}>
|
||||||
<Tooltip visible={isTooltipShowing}>{props.tooltip}</Tooltip>
|
<Tooltip position="left">{props.tooltip}</Tooltip>
|
||||||
<Button
|
<Button href={props.href} target="_blank" rel="noopener noreferrer">
|
||||||
onMouseOver={() => setShowTooltip(true)}
|
|
||||||
onMouseOut={() => setShowTooltip(false)}
|
|
||||||
href={props.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{props.text}
|
{props.text}
|
||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -7,6 +7,7 @@ import { AppState } from '../redux/app';
|
|||||||
import { CommandState } from '../redux/command';
|
import { CommandState } from '../redux/command';
|
||||||
import { DoneLoadingAction, LoadingState } from '../redux/loading';
|
import { DoneLoadingAction, LoadingState } from '../redux/loading';
|
||||||
import { ResultState } from '../redux/result';
|
import { ResultState } from '../redux/result';
|
||||||
|
import { OutputToolbarComponent } from './output-toolbar';
|
||||||
|
|
||||||
const Container = styled.div<{ visible?: boolean }>`
|
const Container = styled.div<{ visible?: boolean }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -15,8 +16,8 @@ const Container = styled.div<{ visible?: boolean }>`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
font-family: Menlo, Monaco, 'Courier New', monospace;
|
font-family: Menlo, Monaco, 'Courier New', monospace;
|
||||||
overflow: scroll;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
transition: transform 0.2s ease-in;
|
transition: transform 0.2s ease-in;
|
||||||
@ -42,9 +43,9 @@ const CancelButton = styled.div`
|
|||||||
|
|
||||||
const Output = styled.div`
|
const Output = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.8em;
|
padding: 0 0.5em 0.5em 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: scroll;
|
||||||
/* This font size is used to calcuate spinner size */
|
/* This font size is used to calcuate spinner size */
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
`;
|
`;
|
||||||
@ -65,6 +66,37 @@ const Pre = styled.pre`
|
|||||||
margin: 0;
|
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: {
|
export const OutputTabComponent = (props: {
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
@ -85,13 +117,14 @@ export const OutputTabComponent = (props: {
|
|||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const outputRef = useRef(null);
|
const outputRef = useRef<HTMLDivElement>(null);
|
||||||
|
const preRef = useRef<HTMLPreElement>(null);
|
||||||
const [spinnerSize, setSpinnerSize] = useState(50);
|
const [spinnerSize, setSpinnerSize] = useState(50);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const htmlElement = (outputRef.current as unknown) as HTMLElement;
|
const outputEl = (outputRef.current as unknown) as HTMLElement;
|
||||||
const fontSize = window
|
const fontSize = window
|
||||||
.getComputedStyle(htmlElement, null)
|
.getComputedStyle(outputEl, null)
|
||||||
.getPropertyValue('font-size');
|
.getPropertyValue('font-size');
|
||||||
|
|
||||||
setSpinnerSize(parseFloat(fontSize) * 3);
|
setSpinnerSize(parseFloat(fontSize) * 3);
|
||||||
@ -99,6 +132,12 @@ export const OutputTabComponent = (props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container visible={props.selected}>
|
<Container visible={props.selected}>
|
||||||
|
{output.length !== 0 && (
|
||||||
|
<OutputToolbarComponent
|
||||||
|
onCopy={() => copyOutput(preRef.current)}
|
||||||
|
onDownload={() => downloadOutput(preRef.current)}
|
||||||
|
></OutputToolbarComponent>
|
||||||
|
)}
|
||||||
<Output id="output" ref={outputRef}>
|
<Output id="output" ref={outputRef}>
|
||||||
{loading.loading && (
|
{loading.loading && (
|
||||||
<LoadingContainer>
|
<LoadingContainer>
|
||||||
@ -122,7 +161,7 @@ export const OutputTabComponent = (props: {
|
|||||||
</LoadingContainer>
|
</LoadingContainer>
|
||||||
)}
|
)}
|
||||||
{!loading.loading &&
|
{!loading.loading &&
|
||||||
((output.length !== 0 && <Pre>{output}</Pre>) ||
|
((output.length !== 0 && <Pre ref={preRef}>{output}</Pre>) ||
|
||||||
(contract.length !== 0 && (
|
(contract.length !== 0 && (
|
||||||
<span>
|
<span>
|
||||||
The contract was successfully deployed to the babylonnet test
|
The contract was successfully deployed to the babylonnet test
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -8,6 +8,7 @@ import styled, { css } from 'styled-components';
|
|||||||
import { AppState } from '../redux/app';
|
import { AppState } from '../redux/app';
|
||||||
import { ChangeShareLinkAction, ShareState } from '../redux/share';
|
import { ChangeShareLinkAction, ShareState } from '../redux/share';
|
||||||
import { share } from '../services/api';
|
import { share } from '../services/api';
|
||||||
|
import { Tooltip } from './tooltip';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
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 = () => {
|
const shareAction = () => {
|
||||||
return async function(dispatch: Dispatch, getState: () => AppState) {
|
return async function(dispatch: Dispatch, getState: () => AppState) {
|
||||||
try {
|
try {
|
||||||
@ -138,7 +119,6 @@ export const ShareComponent = () => {
|
|||||||
state => state.share.link
|
state => state.share.link
|
||||||
);
|
);
|
||||||
const [clicked, setClicked] = useState(false);
|
const [clicked, setClicked] = useState(false);
|
||||||
const [isTooltipShowing, setShowTooltip] = useState(false);
|
|
||||||
|
|
||||||
const SHARE_TOOLTIP = 'Share code';
|
const SHARE_TOOLTIP = 'Share code';
|
||||||
const COPY_TOOLTIP = 'Copy link';
|
const COPY_TOOLTIP = 'Copy link';
|
||||||
@ -149,14 +129,12 @@ export const ShareComponent = () => {
|
|||||||
if (shareLink) {
|
if (shareLink) {
|
||||||
if (inputEl.current && copy(inputEl.current)) {
|
if (inputEl.current && copy(inputEl.current)) {
|
||||||
setTooltipMessage(COPIED_TOOLTIP);
|
setTooltipMessage(COPIED_TOOLTIP);
|
||||||
setShowTooltip(true);
|
|
||||||
} else {
|
} else {
|
||||||
setClicked(true);
|
setClicked(true);
|
||||||
setTooltipMessage(COPY_TOOLTIP);
|
setTooltipMessage(COPY_TOOLTIP);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setClicked(false);
|
setClicked(false);
|
||||||
setShowTooltip(false);
|
|
||||||
setTooltipMessage(SHARE_TOOLTIP);
|
setTooltipMessage(SHARE_TOOLTIP);
|
||||||
}
|
}
|
||||||
}, [shareLink]);
|
}, [shareLink]);
|
||||||
@ -177,9 +155,7 @@ export const ShareComponent = () => {
|
|||||||
if (tooltipMessage === COPIED_TOOLTIP) {
|
if (tooltipMessage === COPIED_TOOLTIP) {
|
||||||
setTooltipMessage(COPY_TOOLTIP);
|
setTooltipMessage(COPY_TOOLTIP);
|
||||||
}
|
}
|
||||||
setShowTooltip(true);
|
|
||||||
}}
|
}}
|
||||||
onMouseOut={() => setShowTooltip(false)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!shareLink) {
|
if (!shareLink) {
|
||||||
dispatch(shareAction());
|
dispatch(shareAction());
|
||||||
@ -193,7 +169,7 @@ export const ShareComponent = () => {
|
|||||||
>
|
>
|
||||||
<Label visible={!clicked}>Share</Label>
|
<Label visible={!clicked}>Share</Label>
|
||||||
<Copy visible={clicked}></Copy>
|
<Copy visible={clicked}></Copy>
|
||||||
<Tooltip visible={isTooltipShowing}>{tooltipMessage}</Tooltip>
|
<Tooltip>{tooltipMessage}</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -69,7 +69,11 @@ export const TabsPanelComponent = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Header>
|
<Header>
|
||||||
{TABS.map(tab => (
|
{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>
|
<Label onClick={() => selectTab(tab)}>{tab.label}</Label>
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
|
104
tools/webide/packages/client/src/components/tooltip.tsx
Normal file
104
tools/webide/packages/client/src/components/tooltip.tsx
Normal 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>;
|
||||||
|
};
|
@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
--tooltip_foreground: white;
|
--tooltip_foreground: white;
|
||||||
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;
|
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;
|
||||||
|
--label_foreground: rgba(153, 153, 153, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
Loading…
Reference in New Issue
Block a user