mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Add line chart and paste dialog selection (#2670)
Co-authored-by: dwelle <luzar.david@gmail.com> Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
parent
c1e2146d78
commit
022f349dc6
15 changed files with 1122 additions and 393 deletions
|
@ -1,181 +1,167 @@
|
|||
import { Point, simplify } from "points-on-curve";
|
||||
import React from "react";
|
||||
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { simplify, Point } from "points-on-curve";
|
||||
|
||||
import {
|
||||
newElement,
|
||||
newTextElement,
|
||||
duplicateElement,
|
||||
isInvisiblySmallElement,
|
||||
isTextElement,
|
||||
textWysiwyg,
|
||||
getCommonBounds,
|
||||
getCursorForResizingElement,
|
||||
getPerfectElementSize,
|
||||
getNormalizedDimensions,
|
||||
newLinearElement,
|
||||
transformElements,
|
||||
getElementWithTransformHandleType,
|
||||
getResizeOffsetXY,
|
||||
getResizeArrowDirection,
|
||||
getTransformHandleTypeFromCoords,
|
||||
isNonDeletedElement,
|
||||
updateTextElement,
|
||||
dragSelectedElements,
|
||||
getDragOffsetXY,
|
||||
dragNewElement,
|
||||
hitTest,
|
||||
isHittingElementBoundingBoxWithoutHittingElement,
|
||||
getNonDeletedElements,
|
||||
} from "../element";
|
||||
import {
|
||||
getElementsWithinSelection,
|
||||
isOverScrollBars,
|
||||
getElementsAtPosition,
|
||||
getElementContainingPosition,
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
isSomeElementSelected,
|
||||
calculateScrollCenter,
|
||||
} from "../scene";
|
||||
import { loadFromBlob, exportCanvas } from "../data";
|
||||
|
||||
import { renderScene } from "../renderer";
|
||||
import {
|
||||
AppState,
|
||||
GestureEvent,
|
||||
Gesture,
|
||||
ExcalidrawProps,
|
||||
SceneData,
|
||||
} from "../types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeleted,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawBindableElement,
|
||||
} from "../element/types";
|
||||
|
||||
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
||||
|
||||
import {
|
||||
isWritableElement,
|
||||
isInputLike,
|
||||
isToolIcon,
|
||||
debounce,
|
||||
distance,
|
||||
resetCursor,
|
||||
viewportCoordsToSceneCoords,
|
||||
sceneCoordsToViewportCoords,
|
||||
setCursorForShape,
|
||||
tupleToCoors,
|
||||
ResolvablePromise,
|
||||
resolvablePromise,
|
||||
withBatchedUpdates,
|
||||
} from "../utils";
|
||||
import {
|
||||
KEYS,
|
||||
isArrowKey,
|
||||
getResizeCenterPointKey,
|
||||
getResizeWithSidesSameLengthKey,
|
||||
getRotateWithDiscreteAngleKey,
|
||||
CODES,
|
||||
} from "../keys";
|
||||
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import { createHistory, SceneHistory } from "../history";
|
||||
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import "../actions";
|
||||
import { actionDeleteSelected, actionFinalize } from "../actions";
|
||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { actions } from "../actions/register";
|
||||
|
||||
import { ActionResult } from "../actions/types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { t, getLanguage } from "../i18n";
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
parseClipboard,
|
||||
probablySupportsClipboardBlob,
|
||||
probablySupportsClipboardWriteText,
|
||||
} from "../clipboard";
|
||||
import { normalizeScroll } from "../scene";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||
|
||||
import {
|
||||
CURSOR_TYPE,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
ELEMENT_TRANSLATE_AMOUNT,
|
||||
POINTER_BUTTON,
|
||||
DRAGGING_THRESHOLD,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
EVENT,
|
||||
ENV,
|
||||
CANVAS_ONLY_ACTIONS,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
GRID_SIZE,
|
||||
MIME_TYPES,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
APP_NAME,
|
||||
} from "../constants";
|
||||
|
||||
import LayerUI from "./LayerUI";
|
||||
import { ScrollBars, SceneState } from "../scene/types";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import {
|
||||
isLinearElement,
|
||||
isLinearElementType,
|
||||
isBindingElement,
|
||||
isBindingElementType,
|
||||
} from "../element/typeChecks";
|
||||
import { actionFinalize, actionDeleteSelected } from "../actions";
|
||||
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
getSelectedGroupIds,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsForSelectedElements,
|
||||
isElementInGroup,
|
||||
getSelectedGroupIdForElement,
|
||||
getElementsInGroup,
|
||||
editGroupForSelectedElement,
|
||||
} from "../groups";
|
||||
import { Library } from "../data/library";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
getHoveredElementForBinding,
|
||||
maybeBindLinearElement,
|
||||
getEligibleElementsForBinding,
|
||||
bindOrUnbindSelectedElements,
|
||||
unbindLinearElements,
|
||||
fixBindingsAfterDuplication,
|
||||
fixBindingsAfterDeletion,
|
||||
isLinearElementSimpleAndAlreadyBound,
|
||||
isBindingEnabled,
|
||||
updateBoundElements,
|
||||
shouldEnableBindingForPointerEvent,
|
||||
} from "../element/binding";
|
||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { renderSpreadsheet } from "../charts";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { restore } from "../data/restore";
|
||||
import {
|
||||
EVENT_DIALOG,
|
||||
EVENT_LIBRARY,
|
||||
EVENT_SHAPE,
|
||||
trackEvent,
|
||||
} from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
copyToClipboard,
|
||||
parseClipboard,
|
||||
probablySupportsClipboardBlob,
|
||||
probablySupportsClipboardWriteText,
|
||||
} from "../clipboard";
|
||||
import {
|
||||
APP_NAME,
|
||||
CANVAS_ONLY_ACTIONS,
|
||||
CURSOR_TYPE,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
DRAGGING_THRESHOLD,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
ELEMENT_TRANSLATE_AMOUNT,
|
||||
ENV,
|
||||
EVENT,
|
||||
GRID_SIZE,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MIME_TYPES,
|
||||
POINTER_BUTTON,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
} from "../constants";
|
||||
import { exportCanvas, loadFromBlob } from "../data";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import { Library } from "../data/library";
|
||||
import { restore } from "../data/restore";
|
||||
import {
|
||||
dragNewElement,
|
||||
dragSelectedElements,
|
||||
duplicateElement,
|
||||
getCommonBounds,
|
||||
getCursorForResizingElement,
|
||||
getDragOffsetXY,
|
||||
getElementWithTransformHandleType,
|
||||
getNonDeletedElements,
|
||||
getNormalizedDimensions,
|
||||
getPerfectElementSize,
|
||||
getResizeArrowDirection,
|
||||
getResizeOffsetXY,
|
||||
getTransformHandleTypeFromCoords,
|
||||
hitTest,
|
||||
isHittingElementBoundingBoxWithoutHittingElement,
|
||||
isInvisiblySmallElement,
|
||||
isNonDeletedElement,
|
||||
isTextElement,
|
||||
newElement,
|
||||
newLinearElement,
|
||||
newTextElement,
|
||||
textWysiwyg,
|
||||
transformElements,
|
||||
updateTextElement,
|
||||
} from "../element";
|
||||
import {
|
||||
bindOrUnbindSelectedElements,
|
||||
fixBindingsAfterDeletion,
|
||||
fixBindingsAfterDuplication,
|
||||
getEligibleElementsForBinding,
|
||||
getHoveredElementForBinding,
|
||||
isBindingEnabled,
|
||||
isLinearElementSimpleAndAlreadyBound,
|
||||
maybeBindLinearElement,
|
||||
shouldEnableBindingForPointerEvent,
|
||||
unbindLinearElements,
|
||||
updateBoundElements,
|
||||
} from "../element/binding";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||
import {
|
||||
isBindingElement,
|
||||
isBindingElementType,
|
||||
isLinearElement,
|
||||
isLinearElementType,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeleted,
|
||||
} from "../element/types";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import {
|
||||
editGroupForSelectedElement,
|
||||
getElementsInGroup,
|
||||
getSelectedGroupIdForElement,
|
||||
getSelectedGroupIds,
|
||||
isElementInGroup,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsForSelectedElements,
|
||||
} from "../groups";
|
||||
import { createHistory, SceneHistory } from "../history";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
import {
|
||||
CODES,
|
||||
getResizeCenterPointKey,
|
||||
getResizeWithSidesSameLengthKey,
|
||||
getRotateWithDiscreteAngleKey,
|
||||
isArrowKey,
|
||||
KEYS,
|
||||
} from "../keys";
|
||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||
import { renderScene } from "../renderer";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import {
|
||||
calculateScrollCenter,
|
||||
getElementContainingPosition,
|
||||
getElementsAtPosition,
|
||||
getElementsWithinSelection,
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
isOverScrollBars,
|
||||
isSomeElementSelected,
|
||||
normalizeScroll,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { SceneState, ScrollBars } from "../scene/types";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import {
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
Gesture,
|
||||
GestureEvent,
|
||||
SceneData,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
distance,
|
||||
isInputLike,
|
||||
isToolIcon,
|
||||
isWritableElement,
|
||||
resetCursor,
|
||||
ResolvablePromise,
|
||||
resolvablePromise,
|
||||
sceneCoordsToViewportCoords,
|
||||
setCursorForShape,
|
||||
tupleToCoors,
|
||||
viewportCoordsToSceneCoords,
|
||||
withBatchedUpdates,
|
||||
} from "../utils";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Stats } from "./Stats";
|
||||
|
||||
const { history } = createHistory();
|
||||
|
@ -374,7 +360,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
elements={this.scene.getElements()}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={this.toggleLock}
|
||||
onInsertShape={(elements) =>
|
||||
onInsertElements={(elements) =>
|
||||
this.addElementsFromPasteOrLibrary(
|
||||
elements,
|
||||
DEFAULT_PASTE_X,
|
||||
|
@ -1004,9 +990,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
if (data.errorMessage) {
|
||||
this.setState({ errorMessage: data.errorMessage });
|
||||
} else if (data.spreadsheet) {
|
||||
this.addElementsFromPasteOrLibrary(
|
||||
renderSpreadsheet(data.spreadsheet, cursorX, cursorY),
|
||||
);
|
||||
this.setState({
|
||||
pasteDialog: {
|
||||
data: data.spreadsheet,
|
||||
shown: true,
|
||||
},
|
||||
});
|
||||
} else if (data.elements) {
|
||||
this.addElementsFromPasteOrLibrary(data.elements);
|
||||
} else if (data.text) {
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Modal } from "./Modal";
|
||||
import { Island } from "./Island";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { back, close } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
import "./Dialog.scss";
|
||||
import { back, close } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
const useRefState = <T,>() => {
|
||||
const [refValue, setRefValue] = useState<T | null>(null);
|
||||
|
@ -23,6 +22,7 @@ export const Dialog = (props: {
|
|||
maxWidth?: number;
|
||||
onCloseRequest(): void;
|
||||
title: React.ReactNode;
|
||||
autofocus?: boolean;
|
||||
}) => {
|
||||
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
||||
|
||||
|
@ -33,7 +33,7 @@ export const Dialog = (props: {
|
|||
|
||||
const focusableElements = queryFocusableElements(islandNode);
|
||||
|
||||
if (focusableElements.length > 0) {
|
||||
if (focusableElements.length > 0 && props.autofocus !== false) {
|
||||
// If there's an element other than close, focus it.
|
||||
(focusableElements[1] || focusableElements[0]).focus();
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export const Dialog = (props: {
|
|||
islandNode.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||
}, [islandNode]);
|
||||
}, [islandNode, props.autofocus]);
|
||||
|
||||
const queryFocusableElements = (node: HTMLElement) => {
|
||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
||||
|
|
|
@ -51,6 +51,7 @@ import {
|
|||
EVENT_LIBRARY,
|
||||
trackEvent,
|
||||
} from "../analytics";
|
||||
import { PasteChartDialog } from "./PasteChartDialog";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
|
@ -60,7 +61,7 @@ interface LayerUIProps {
|
|||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onInsertShape: (elements: LibraryItem) => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
toggleZenMode: () => void;
|
||||
lng: string;
|
||||
|
@ -318,7 +319,7 @@ const LayerUI = ({
|
|||
elements,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
onInsertShape,
|
||||
onInsertElements,
|
||||
zenModeEnabled,
|
||||
toggleZenMode,
|
||||
isCollaborating,
|
||||
|
@ -456,7 +457,7 @@ const LayerUI = ({
|
|||
<LibraryMenu
|
||||
pendingElements={getSelectedElements(elements, appState)}
|
||||
onClickOutside={closeLibrary}
|
||||
onInsertShape={onInsertShape}
|
||||
onInsertShape={onInsertElements}
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
|
@ -592,21 +593,8 @@ const LayerUI = ({
|
|||
</footer>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
exportButton={renderExportDialog()}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={onLockToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
/>
|
||||
) : (
|
||||
<div className="layer-ui__wrapper">
|
||||
const dialogs = (
|
||||
<>
|
||||
{appState.isLoading && <LoadingMessage />}
|
||||
{appState.errorMessage && (
|
||||
<ErrorDialog
|
||||
|
@ -619,6 +607,40 @@ const LayerUI = ({
|
|||
onClose={() => setAppState({ showShortcutsDialog: false })}
|
||||
/>
|
||||
)}
|
||||
{appState.pasteDialog.shown && (
|
||||
<PasteChartDialog
|
||||
setAppState={setAppState}
|
||||
appState={appState}
|
||||
onInsertChart={onInsertElements}
|
||||
onClose={() =>
|
||||
setAppState({
|
||||
pasteDialog: { shown: false, data: null },
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<>
|
||||
{dialogs}
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
exportButton={renderExportDialog()}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={onLockToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="layer-ui__wrapper">
|
||||
{dialogs}
|
||||
{renderFixedSideContainer()}
|
||||
{renderBottomAppMenu()}
|
||||
{
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useRef, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import oc from "open-color";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { close } from "../components/icons";
|
||||
|
||||
import "./LibraryUnit.scss";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { LibraryItem } from "../types";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import "./LibraryUnit.scss";
|
||||
|
||||
// fa-plus
|
||||
const PLUS_ICON = (
|
||||
|
@ -38,7 +38,7 @@ export const LibraryUnit = ({
|
|||
}
|
||||
const svg = exportToSvg(elementsToRender, {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: "#fff",
|
||||
viewBackgroundColor: oc.white,
|
||||
shouldAddWatermark: false,
|
||||
});
|
||||
for (const child of ref.current!.children) {
|
||||
|
|
|
@ -15,7 +15,6 @@ import { Section } from "./Section";
|
|||
import CollabButton from "./CollabButton";
|
||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||
import { LockIcon } from "./LockIcon";
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { UserList } from "./UserList";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||
|
@ -46,7 +45,6 @@ export const MobileMenu = ({
|
|||
isCollaborating,
|
||||
}: MobileMenuProps) => (
|
||||
<>
|
||||
{appState.isLoading && <LoadingMessage />}
|
||||
<FixedSideContainer side="top">
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
|
|
46
src/components/PasteChartDialog.scss
Normal file
46
src/components/PasteChartDialog.scss
Normal file
|
@ -0,0 +1,46 @@
|
|||
@import "../css/_variables";
|
||||
|
||||
.excalidraw {
|
||||
.PasteChartDialog {
|
||||
@media #{$media-query} {
|
||||
.Island {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
@media #{$media-query} {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.ChartPreview {
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
width: 192px;
|
||||
height: 128px;
|
||||
border-radius: 2px;
|
||||
padding: 1px;
|
||||
border: 1px solid $oc-gray-4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
svg {
|
||||
max-height: 120px;
|
||||
max-width: 186px;
|
||||
}
|
||||
&:hover {
|
||||
padding: 0;
|
||||
border: 2px solid $oc-blue-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
src/components/PasteChartDialog.tsx
Normal file
121
src/components/PasteChartDialog.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import oc from "open-color";
|
||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||
import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||
import { ChartType } from "../element/types";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { AppState, LibraryItem } from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import "./PasteChartDialog.scss";
|
||||
|
||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||
|
||||
const ChartPreviewBtn = (props: {
|
||||
spreadsheet: Spreadsheet | null;
|
||||
chartType: ChartType;
|
||||
selected: boolean;
|
||||
onClick: OnInsertChart;
|
||||
}) => {
|
||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!props.spreadsheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = renderSpreadsheet(
|
||||
props.chartType,
|
||||
props.spreadsheet,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
setChartElements(elements);
|
||||
|
||||
const svg = exportToSvg(elements, {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
shouldAddWatermark: false,
|
||||
});
|
||||
|
||||
const previewNode = previewRef.current!;
|
||||
|
||||
previewNode.appendChild(svg);
|
||||
|
||||
if (props.selected) {
|
||||
(previewNode.parentNode as HTMLDivElement).focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
previewNode.removeChild(svg);
|
||||
};
|
||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="ChartPreview"
|
||||
onClick={() => {
|
||||
if (chartElements) {
|
||||
props.onClick(props.chartType, chartElements);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div ref={previewRef} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const PasteChartDialog = ({
|
||||
setAppState,
|
||||
appState,
|
||||
onClose,
|
||||
onInsertChart,
|
||||
}: {
|
||||
appState: AppState;
|
||||
onClose: () => void;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onInsertChart: (elements: LibraryItem) => void;
|
||||
}) => {
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||
onInsertChart(elements);
|
||||
setAppState({
|
||||
currentChartType: chartType,
|
||||
pasteDialog: {
|
||||
shown: false,
|
||||
data: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
maxWidth={500}
|
||||
onCloseRequest={handleClose}
|
||||
title={"Paste chart"}
|
||||
className={"PasteChartDialog"}
|
||||
autofocus={false}
|
||||
>
|
||||
<div className={"container"}>
|
||||
<ChartPreviewBtn
|
||||
chartType="bar"
|
||||
spreadsheet={appState.pasteDialog.data}
|
||||
selected={appState.currentChartType === "bar"}
|
||||
onClick={handleChartClick}
|
||||
/>
|
||||
<ChartPreviewBtn
|
||||
chartType="line"
|
||||
spreadsheet={appState.pasteDialog.data}
|
||||
selected={appState.currentChartType === "line"}
|
||||
onClick={handleChartClick}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -85,7 +85,6 @@ export const Stats = (props: {
|
|||
<td>{t("stats.total")}</td>
|
||||
<td>{nFormatter(storageSizes.total, 1)}</td>
|
||||
</tr>
|
||||
|
||||
{selectedElements.length === 1 && (
|
||||
<tr>
|
||||
<th colSpan={2}>{t("stats.element")}</th>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue