From ddd148a2906fc5d7592dcd46f1c00071ae48f5db Mon Sep 17 00:00:00 2001 From: Maksym Bykovskyy Date: Sat, 15 Feb 2020 17:49:46 -0800 Subject: [PATCH] Added copy and download to output. Improved tooltip component --- tools/webide/packages/client/src/App.tsx | 2 + .../client/src/components/float-button.tsx | 40 +------ .../client/src/components/output-tab.tsx | 53 +++++++-- .../client/src/components/output-toolbar.tsx | 78 +++++++++++++ .../packages/client/src/components/share.tsx | 28 +---- .../client/src/components/tabs-panel.tsx | 6 +- .../client/src/components/tooltip.tsx | 104 ++++++++++++++++++ tools/webide/packages/client/src/index.css | 1 + 8 files changed, 244 insertions(+), 68 deletions(-) create mode 100644 tools/webide/packages/client/src/components/output-toolbar.tsx create mode 100644 tools/webide/packages/client/src/components/tooltip.tsx diff --git a/tools/webide/packages/client/src/App.tsx b/tools/webide/packages/client/src/App.tsx index fb269a6ee..ed379ef8e 100644 --- a/tools/webide/packages/client/src/App.tsx +++ b/tools/webide/packages/client/src/App.tsx @@ -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" > + ); }; diff --git a/tools/webide/packages/client/src/components/float-button.tsx b/tools/webide/packages/client/src/components/float-button.tsx index cff64cb30..0b2aea42b 100644 --- a/tools/webide/packages/client/src/components/float-button.tsx +++ b/tools/webide/packages/client/src/components/float-button.tsx @@ -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 ( - {props.tooltip} - diff --git a/tools/webide/packages/client/src/components/output-tab.tsx b/tools/webide/packages/client/src/components/output-tab.tsx index 9322ebd0c..0f6cad87e 100644 --- a/tools/webide/packages/client/src/components/output-tab.tsx +++ b/tools/webide/packages/client/src/components/output-tab.tsx @@ -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(null); + const preRef = useRef(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 ( + {output.length !== 0 && ( + copyOutput(preRef.current)} + onDownload={() => downloadOutput(preRef.current)} + > + )} {loading.loading && ( @@ -122,7 +161,7 @@ export const OutputTabComponent = (props: { )} {!loading.loading && - ((output.length !== 0 &&
{output}
) || + ((output.length !== 0 &&
{output}
) || (contract.length !== 0 && ( The contract was successfully deployed to the babylonnet test diff --git a/tools/webide/packages/client/src/components/output-toolbar.tsx b/tools/webide/packages/client/src/components/output-toolbar.tsx new file mode 100644 index 000000000..a258352ce --- /dev/null +++ b/tools/webide/packages/client/src/components/output-toolbar.tsx @@ -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 ( + + props.onCopy && props.onCopy()}> + + Copy + + props.onDownload && props.onDownload()}> + + Download + + + ); +}; diff --git a/tools/webide/packages/client/src/components/share.tsx b/tools/webide/packages/client/src/components/share.tsx index fb919ad3f..bb81591eb 100644 --- a/tools/webide/packages/client/src/components/share.tsx +++ b/tools/webide/packages/client/src/components/share.tsx @@ -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 = () => { > - {tooltipMessage} + {tooltipMessage}
); diff --git a/tools/webide/packages/client/src/components/tabs-panel.tsx b/tools/webide/packages/client/src/components/tabs-panel.tsx index 436ace1fc..8714dd9bf 100644 --- a/tools/webide/packages/client/src/components/tabs-panel.tsx +++ b/tools/webide/packages/client/src/components/tabs-panel.tsx @@ -69,7 +69,11 @@ export const TabsPanelComponent = () => {
{TABS.map(tab => ( - + ))} diff --git a/tools/webide/packages/client/src/components/tooltip.tsx b/tools/webide/packages/client/src/components/tooltip.tsx new file mode 100644 index 000000000..6bbef8e27 --- /dev/null +++ b/tools/webide/packages/client/src/components/tooltip.tsx @@ -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 ; +}; + +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(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
; +}; diff --git a/tools/webide/packages/client/src/index.css b/tools/webide/packages/client/src/index.css index 5dfa73718..aaa5a1f7b 100644 --- a/tools/webide/packages/client/src/index.css +++ b/tools/webide/packages/client/src/index.css @@ -50,6 +50,7 @@ --tooltip_foreground: white; --tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/; + --label_foreground: rgba(153, 153, 153, 1); } body {