mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
485: Ability to switch to previously loaded ids in UI (#583)
This commit is contained in:
parent
bd1c00014b
commit
4ad38e317e
2016
package-lock.json
generated
2016
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,11 +19,15 @@
|
|||||||
},
|
},
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/enzyme": "3.10.4",
|
||||||
|
"@types/enzyme-adapter-react-16": "1.0.5",
|
||||||
"@types/jest": "25.1.0",
|
"@types/jest": "25.1.0",
|
||||||
"@types/nanoid": "2.1.0",
|
"@types/nanoid": "2.1.0",
|
||||||
"@types/react": "16.9.19",
|
"@types/react": "16.9.19",
|
||||||
"@types/react-color": "3.0.1",
|
"@types/react-color": "3.0.1",
|
||||||
"@types/react-dom": "16.9.5",
|
"@types/react-dom": "16.9.5",
|
||||||
|
"enzyme": "3.11.0",
|
||||||
|
"enzyme-adapter-react-16": "1.15.2",
|
||||||
"husky": "4.2.1",
|
"husky": "4.2.1",
|
||||||
"lint-staged": "10.0.3",
|
"lint-staged": "10.0.3",
|
||||||
"node-sass": "4.13.1",
|
"node-sass": "4.13.1",
|
||||||
|
@ -51,7 +51,8 @@
|
|||||||
"load": "Load",
|
"load": "Load",
|
||||||
"getShareableLink": "Get shareable link",
|
"getShareableLink": "Get shareable link",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"selectLanguage": "Select Language"
|
"selectLanguage": "Select Language",
|
||||||
|
"previouslyLoadedScenes": "Previously loaded scenes"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"getShareableLink": "Obtener enlace para compartir",
|
"getShareableLink": "Obtener enlace para compartir",
|
||||||
"showExportDialog": "Mostrar diálogo para exportar",
|
"showExportDialog": "Mostrar diálogo para exportar",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
"selectLanguage": "Seleccionar idioma"
|
"selectLanguage": "Seleccionar idioma",
|
||||||
|
"previouslyLoadedScenes": "Escenas previamente cargadas"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
|
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
|
||||||
|
@ -45,7 +45,8 @@
|
|||||||
"copyToClipboard": "Copier dans le presse-papier",
|
"copyToClipboard": "Copier dans le presse-papier",
|
||||||
"save": "Sauvegarder",
|
"save": "Sauvegarder",
|
||||||
"load": "Ouvrir",
|
"load": "Ouvrir",
|
||||||
"getShareableLink": "Obtenir un lien de partage"
|
"getShareableLink": "Obtenir un lien de partage",
|
||||||
|
"previouslyLoadedScenes": "Scènes précédemment chargées"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "L'intégralité du canvas va être effacé. Êtes-vous sur ?",
|
"clearReset": "L'intégralité du canvas va être effacé. Êtes-vous sur ?",
|
||||||
|
@ -45,7 +45,8 @@
|
|||||||
"copyToClipboard": "Copiar para o clipboard",
|
"copyToClipboard": "Copiar para o clipboard",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"load": "Carregar",
|
"load": "Carregar",
|
||||||
"getShareableLink": "Obter um link de partilha"
|
"getShareableLink": "Obter um link de partilha",
|
||||||
|
"previouslyLoadedScenes": "Cenas carregadas anteriormente"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "O canvas inteiro será excluído. Tens a certeza?",
|
"clearReset": "O canvas inteiro será excluído. Tens a certeza?",
|
||||||
|
@ -51,7 +51,8 @@
|
|||||||
"load": "Загрузить",
|
"load": "Загрузить",
|
||||||
"getShareableLink": "Получить доступ по ссылке",
|
"getShareableLink": "Получить доступ по ссылке",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"selectLanguage": "Выбрать язык"
|
"selectLanguage": "Выбрать язык",
|
||||||
|
"previouslyLoadedScenes": "Ранее загруженные сцены"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Это очистит весь холст. Вы уверены?",
|
"clearReset": "Это очистит весь холст. Вы уверены?",
|
||||||
|
56
src/components/StoredScenesList.test.tsx
Normal file
56
src/components/StoredScenesList.test.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Enzyme, { shallow } from "enzyme";
|
||||||
|
import Adapter from "enzyme-adapter-react-16";
|
||||||
|
import { StoredScenesList } from "./StoredScenesList";
|
||||||
|
import { PreviousScene } from "../scene/types";
|
||||||
|
|
||||||
|
Enzyme.configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
|
jest.mock("react-i18next", () => ({
|
||||||
|
useTranslation: () => ({ t: (key: any) => key }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function setup(props: any) {
|
||||||
|
const currentProps = {
|
||||||
|
...props,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
wrapper: shallow(<StoredScenesList {...currentProps} />),
|
||||||
|
props: currentProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("<StoredScenesList/>", () => {
|
||||||
|
const scenes: PreviousScene[] = [
|
||||||
|
{
|
||||||
|
id: "123",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "234",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "345",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { wrapper, props } = setup({ scenes });
|
||||||
|
|
||||||
|
describe("Renders the ids correctly when", () => {
|
||||||
|
it("select options and ids length are the same", () => {
|
||||||
|
expect(wrapper.find("option").length).toBe(scenes.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Can handle id selection when", () => {
|
||||||
|
it("onChange method is called when select option has changed", async () => {
|
||||||
|
const select = wrapper.find("select") as any;
|
||||||
|
const mockedEvenet = { currentTarget: { value: "1" } };
|
||||||
|
await select.invoke("onChange")(mockedEvenet);
|
||||||
|
expect(props.onChange.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
34
src/components/StoredScenesList.tsx
Normal file
34
src/components/StoredScenesList.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { PreviousScene } from "../scene/types";
|
||||||
|
|
||||||
|
interface StoredScenesListProps {
|
||||||
|
scenes: PreviousScene[];
|
||||||
|
currentId?: string;
|
||||||
|
onChange: (selectedId: string) => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoredScenesList({
|
||||||
|
scenes,
|
||||||
|
currentId,
|
||||||
|
onChange,
|
||||||
|
}: StoredScenesListProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<select
|
||||||
|
className="stored-ids-select"
|
||||||
|
onChange={({ currentTarget }) => onChange(currentTarget.value)}
|
||||||
|
value={currentId}
|
||||||
|
title={t("buttons.previouslyLoadedScenes")}
|
||||||
|
>
|
||||||
|
{scenes.map(scene => (
|
||||||
|
<option key={scene.id} value={scene.id}>
|
||||||
|
id={scene.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
@ -34,6 +34,8 @@ import {
|
|||||||
hasText,
|
hasText,
|
||||||
exportCanvas,
|
exportCanvas,
|
||||||
importFromBackend,
|
importFromBackend,
|
||||||
|
addToLoadedScenes,
|
||||||
|
loadedScenes,
|
||||||
} from "./scene";
|
} from "./scene";
|
||||||
|
|
||||||
import { renderScene } from "./renderer";
|
import { renderScene } from "./renderer";
|
||||||
@ -86,6 +88,7 @@ import { ExportDialog } from "./components/ExportDialog";
|
|||||||
import { withTranslation } from "react-i18next";
|
import { withTranslation } from "react-i18next";
|
||||||
import { LanguageList } from "./components/LanguageList";
|
import { LanguageList } from "./components/LanguageList";
|
||||||
import i18n, { languages, parseDetectedLang } from "./i18n";
|
import i18n, { languages, parseDetectedLang } from "./i18n";
|
||||||
|
import { StoredScenesList } from "./components/StoredScenesList";
|
||||||
|
|
||||||
let { elements } = createScene();
|
let { elements } = createScene();
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
@ -237,21 +240,13 @@ export class App extends React.Component<any, AppState> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
private async loadScene(id: string | null) {
|
||||||
document.addEventListener("copy", this.onCopy);
|
|
||||||
document.addEventListener("paste", this.onPaste);
|
|
||||||
document.addEventListener("cut", this.onCut);
|
|
||||||
|
|
||||||
document.addEventListener("keydown", this.onKeyDown, false);
|
|
||||||
document.addEventListener("mousemove", this.updateCurrentCursorPosition);
|
|
||||||
window.addEventListener("resize", this.onResize, false);
|
|
||||||
window.addEventListener("unload", this.onUnload, false);
|
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
let selectedId;
|
||||||
|
if (id != null) {
|
||||||
if (searchParams.get("id") != null) {
|
data = await importFromBackend(id);
|
||||||
data = await importFromBackend(searchParams.get("id"));
|
addToLoadedScenes(id);
|
||||||
|
selectedId = id;
|
||||||
window.history.replaceState({}, "Excalidraw", window.location.origin);
|
window.history.replaceState({}, "Excalidraw", window.location.origin);
|
||||||
} else {
|
} else {
|
||||||
data = restoreFromLocalStorage();
|
data = restoreFromLocalStorage();
|
||||||
@ -262,12 +257,28 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.appState) {
|
if (data.appState) {
|
||||||
this.setState(data.appState);
|
this.setState({ ...data.appState, selectedId });
|
||||||
} else {
|
} else {
|
||||||
this.setState({});
|
this.setState({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
document.addEventListener("copy", this.onCopy);
|
||||||
|
document.addEventListener("paste", this.onPaste);
|
||||||
|
document.addEventListener("cut", this.onCut);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", this.onKeyDown, false);
|
||||||
|
document.addEventListener("mousemove", this.updateCurrentCursorPosition);
|
||||||
|
window.addEventListener("resize", this.onResize, false);
|
||||||
|
window.addEventListener("unload", this.onUnload, false);
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
this.loadScene(id);
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
document.removeEventListener("copy", this.onCopy);
|
document.removeEventListener("copy", this.onCopy);
|
||||||
document.removeEventListener("paste", this.onPaste);
|
document.removeEventListener("paste", this.onPaste);
|
||||||
@ -1420,11 +1431,26 @@ export class App extends React.Component<any, AppState> {
|
|||||||
languages={languages}
|
languages={languages}
|
||||||
currentLanguage={parseDetectedLang(i18n.language)}
|
currentLanguage={parseDetectedLang(i18n.language)}
|
||||||
/>
|
/>
|
||||||
|
{this.renderIdsDropdown()}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderIdsDropdown() {
|
||||||
|
const scenes = loadedScenes();
|
||||||
|
if (scenes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<StoredScenesList
|
||||||
|
scenes={scenes}
|
||||||
|
currentId={this.state.selectedId}
|
||||||
|
onChange={id => this.loadScene(id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private handleWheel = (e: WheelEvent) => {
|
private handleWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { deltaX, deltaY } = e;
|
const { deltaX, deltaY } = e;
|
||||||
|
@ -3,7 +3,7 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { ExportType } from "./types";
|
import { ExportType, PreviousScene } from "./types";
|
||||||
import { exportToCanvas, exportToSvg } from "./export";
|
import { exportToCanvas, exportToSvg } from "./export";
|
||||||
import nanoid from "nanoid";
|
import nanoid from "nanoid";
|
||||||
import { fileOpen, fileSave } from "browser-nativefs";
|
import { fileOpen, fileSave } from "browser-nativefs";
|
||||||
@ -12,6 +12,7 @@ import { getCommonBounds } from "../element";
|
|||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "excalidraw";
|
const LOCAL_STORAGE_KEY = "excalidraw";
|
||||||
|
const LOCAL_STORAGE_SCENE_PREVIOUS_KEY = "excalidraw-previos-scenes";
|
||||||
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
|
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
|
||||||
const BACKEND_POST = "https://json.excalidraw.com/api/v1/post/";
|
const BACKEND_POST = "https://json.excalidraw.com/api/v1/post/";
|
||||||
const BACKEND_GET = "https://json.excalidraw.com/api/v1/";
|
const BACKEND_GET = "https://json.excalidraw.com/api/v1/";
|
||||||
@ -24,6 +25,7 @@ const BACKEND_GET = "https://json.excalidraw.com/api/v1/";
|
|||||||
interface DataState {
|
interface DataState {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
|
selectedId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeAsJSON(
|
export function serializeAsJSON(
|
||||||
@ -305,3 +307,43 @@ export function saveToLocalStorage(
|
|||||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY_STATE, JSON.stringify(state));
|
localStorage.setItem(LOCAL_STORAGE_KEY_STATE, JSON.stringify(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of ids in Local Storage
|
||||||
|
* @returns array
|
||||||
|
*/
|
||||||
|
export function loadedScenes(): PreviousScene[] {
|
||||||
|
const storedPreviousScenes = localStorage.getItem(
|
||||||
|
LOCAL_STORAGE_SCENE_PREVIOUS_KEY,
|
||||||
|
);
|
||||||
|
if (storedPreviousScenes) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(storedPreviousScenes);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not parse previously stored ids");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append id to the list of Previous Scenes in Local Storage if not there yet
|
||||||
|
* @param id string
|
||||||
|
*/
|
||||||
|
export function addToLoadedScenes(id: string): void {
|
||||||
|
const scenes = [...loadedScenes()];
|
||||||
|
const newScene = scenes.every(scene => scene.id !== id);
|
||||||
|
|
||||||
|
if (newScene) {
|
||||||
|
scenes.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
LOCAL_STORAGE_SCENE_PREVIOUS_KEY,
|
||||||
|
JSON.stringify(scenes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -15,6 +15,8 @@ export {
|
|||||||
saveToLocalStorage,
|
saveToLocalStorage,
|
||||||
exportToBackend,
|
exportToBackend,
|
||||||
importFromBackend,
|
importFromBackend,
|
||||||
|
addToLoadedScenes,
|
||||||
|
loadedScenes,
|
||||||
} from "./data";
|
} from "./data";
|
||||||
export {
|
export {
|
||||||
hasBackground,
|
hasBackground,
|
||||||
|
@ -16,4 +16,9 @@ export interface Scene {
|
|||||||
elements: ExcalidrawTextElement[];
|
elements: ExcalidrawTextElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreviousScene {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ExportType = "png" | "clipboard" | "backend" | "svg";
|
export type ExportType = "png" | "clipboard" | "backend" | "svg";
|
||||||
|
@ -213,13 +213,13 @@ button,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-select {
|
.dropdown-select {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
height: 1.5rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 1.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
padding: 0 1.5rem 0 0.5rem;
|
padding: 0 1.5rem 0 0.5rem;
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
@ -245,6 +245,21 @@ button,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-select {
|
||||||
|
@extend .dropdown-select;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stored-ids-select {
|
||||||
|
@extend .dropdown-select;
|
||||||
|
padding: 0 0.5em 0 1.7em;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-position: left 0.7em top 50%, 0 0;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
@ -22,4 +22,5 @@ export type AppState = {
|
|||||||
cursorX: number;
|
cursorX: number;
|
||||||
cursorY: number;
|
cursorY: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
selectedId?: string;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user