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)}
+ >
+ )}
);
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 {