mirror of
https://github.com/excalidraw/excalidraw-webex.git
synced 2026-05-17 13:30:45 +00:00
feat: Stop showing collab button and always start with collaboration (#4)
* feat: Stop showing collab button and always start with collaboration * open export dialog when session closes * wait for collabApi to be initialized before intiating collab * fix * add mobile checks for opening export dialog * fix * fix * fix
This commit is contained in:
+49
-6
@@ -20,7 +20,7 @@ import {
|
||||
import { ImportedDataState } from "@excalidraw/excalidraw/types/data/types";
|
||||
import { getCollaborationLinkData } from "./data";
|
||||
import { ResolvablePromise } from "@excalidraw/excalidraw/types/utils";
|
||||
import { loadScript, resolvablePromise } from "./utils";
|
||||
import { isDev, loadScript, resolvablePromise } from "./utils";
|
||||
import { WEBEX_URL } from "./constants";
|
||||
|
||||
const ExcalidrawWrapper = () => {
|
||||
@@ -46,7 +46,24 @@ const ExcalidrawWrapper = () => {
|
||||
window.webexInstance = new window.Webex.Application();
|
||||
const webexApp = window.webexInstance;
|
||||
|
||||
if (!collabAPI || !excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initiateCollab = () => {
|
||||
const roomLinkData = getCollaborationLinkData(window.location.href);
|
||||
if (!roomLinkData) {
|
||||
collabAPI?.initializeSocketClient(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial collab session manually as webex onReady will not be triggered in dev mode
|
||||
if (isDev()) {
|
||||
initiateCollab();
|
||||
}
|
||||
|
||||
webexApp.onReady().then(() => {
|
||||
initiateCollab();
|
||||
const currentTheme = webexApp.theme.toLowerCase();
|
||||
if (currentTheme !== theme) {
|
||||
setTheme(currentTheme);
|
||||
@@ -63,14 +80,41 @@ const ExcalidrawWrapper = () => {
|
||||
webexApp.on("application:themeChanged", (theme: "LIGHT" | "DARK") => {
|
||||
setTheme(theme.toLowerCase() as Theme);
|
||||
});
|
||||
|
||||
webexApp.on("application:shareStateChanged", (isShared: boolean) => {
|
||||
// Open json export modal if sharing turned off
|
||||
if (!isShared) {
|
||||
let exportButton = document.querySelector(
|
||||
'[data-testid="json-export-button"]',
|
||||
) as HTMLElement;
|
||||
|
||||
// This will happen for mobile view
|
||||
if (exportButton === null) {
|
||||
// open mobile menu => click on export => close mobile menu
|
||||
const currentAppState = excalidrawAPI.getAppState();
|
||||
excalidrawAPI.updateScene({
|
||||
appState: { ...currentAppState, openMenu: "canvas" },
|
||||
});
|
||||
exportButton = document.querySelector(
|
||||
'[data-testid="json-export-button"]',
|
||||
) as HTMLElement;
|
||||
}
|
||||
exportButton.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
loadScript(WEBEX_URL).then(() => {
|
||||
|
||||
if (!window.webexInstance) {
|
||||
loadScript(WEBEX_URL).then(() => {
|
||||
initializeWebex();
|
||||
setLoaded(true);
|
||||
});
|
||||
} else {
|
||||
initializeWebex();
|
||||
setLoaded(true);
|
||||
});
|
||||
}, [theme]);
|
||||
}
|
||||
}, [theme, excalidrawAPI, collabAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!collabAPI || !excalidrawAPI) {
|
||||
@@ -107,7 +151,6 @@ const ExcalidrawWrapper = () => {
|
||||
<Excalidraw
|
||||
ref={excalidrawRefCallback}
|
||||
onChange={onChange}
|
||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||
isCollaborating={collabAPI?.isCollaborating()}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Gesture,
|
||||
} from "@excalidraw/excalidraw/types/types";
|
||||
import { EVENT } from "../constants";
|
||||
import RoomDialog from "./RoomDialog";
|
||||
import {
|
||||
getElementMap,
|
||||
getSceneVersion,
|
||||
@@ -33,8 +32,6 @@ import {
|
||||
import { resolvablePromise } from "../utils";
|
||||
|
||||
interface CollabState {
|
||||
modalIsShown: boolean;
|
||||
activeRoomLink: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
@@ -43,7 +40,6 @@ type CollabInstance = InstanceType<typeof CollabWrapper>;
|
||||
export interface CollabAPI {
|
||||
isCollaborating: () => boolean;
|
||||
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
||||
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
||||
broadcastElements: CollabInstance["broadcastElements"];
|
||||
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
||||
}
|
||||
@@ -83,8 +79,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
modalIsShown: false,
|
||||
activeRoomLink: "",
|
||||
errorMessage: "",
|
||||
};
|
||||
|
||||
@@ -104,12 +98,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.destroySocketClient({ isUnload: true });
|
||||
};
|
||||
|
||||
onCollabButtonClick = () => {
|
||||
this.setState({
|
||||
modalIsShown: true,
|
||||
});
|
||||
};
|
||||
|
||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
if (
|
||||
getSceneVersion(elements) >
|
||||
@@ -147,9 +135,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: this.collaborators,
|
||||
});
|
||||
this.setState({
|
||||
activeRoomLink: "",
|
||||
});
|
||||
|
||||
window.webexInstance.clearShareUrl();
|
||||
this.isCollaborating = false;
|
||||
}
|
||||
@@ -304,14 +290,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
scenePromise.resolve(null);
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
activeRoomLink: window.location.href,
|
||||
},
|
||||
() => {
|
||||
window.webexInstance.setShareUrl(this.state.activeRoomLink);
|
||||
},
|
||||
);
|
||||
window.webexInstance.setShareUrl(window.location.href);
|
||||
|
||||
return scenePromise;
|
||||
};
|
||||
@@ -383,10 +362,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
return newElements as ReconciledElements;
|
||||
};
|
||||
|
||||
openPortal = async () => {
|
||||
return this.initializeSocketClient(null);
|
||||
};
|
||||
|
||||
closePortal = () => {
|
||||
window.history.pushState({}, APP_NAME, window.location.origin);
|
||||
this.destroySocketClient();
|
||||
@@ -443,28 +418,13 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
this.contextValue.isCollaborating = () => this.isCollaborating;
|
||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
||||
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
|
||||
this.contextValue.broadcastElements = this.broadcastElements;
|
||||
return this.contextValue;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { activeRoomLink, modalIsShown } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalIsShown && (
|
||||
<RoomDialog
|
||||
handleClose={() => this.setState({ modalIsShown: false })}
|
||||
activeRoomLink={activeRoomLink}
|
||||
onRoomCreate={this.openPortal}
|
||||
onRoomDestroy={this.closePortal}
|
||||
setErrorMessage={(errorMessage: string) => {
|
||||
this.setState({ errorMessage });
|
||||
}}
|
||||
theme={this.excalidrawAPI.getAppState().theme}
|
||||
/>
|
||||
)}
|
||||
<CollabContextProvider
|
||||
value={{
|
||||
api: this.getContextValue(),
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
@import "../css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.RoomDialog-linkContainer {
|
||||
display: flex;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
.RoomDialog-link {
|
||||
color: var(--text-primary-color);
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
padding: 0 0.5rem;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--space-factor);
|
||||
background-color: var(--button-gray-1);
|
||||
}
|
||||
|
||||
.RoomDialog-emoji {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.RoomDialog-usernameContainer {
|
||||
display: flex;
|
||||
margin: 1.5em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include isMobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
.RoomDialog-usernameLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.RoomDialog-username {
|
||||
background-color: var(--input-bg-color);
|
||||
border-color: var(--input-border-color);
|
||||
appearance: none;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
@include isMobile {
|
||||
margin-top: 0.5em;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
height: 2.5rem;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.RoomDialog-sessionStartButtonContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Modal .RoomDialog-stopSession {
|
||||
background-color: var(--button-destructive-bg-color);
|
||||
|
||||
.ToolIcon__label,
|
||||
.ToolIcon__icon svg {
|
||||
color: var(--button-destructive-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { AppState } from "@excalidraw/excalidraw/types/types";
|
||||
import React, { useRef } from "react";
|
||||
import { start, stop } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { Dialog } from "../components/Dialog";
|
||||
import "./RoomDialog.scss";
|
||||
|
||||
const RoomDialog = ({
|
||||
handleClose,
|
||||
activeRoomLink,
|
||||
onRoomCreate,
|
||||
onRoomDestroy,
|
||||
setErrorMessage,
|
||||
theme,
|
||||
}: {
|
||||
handleClose: () => void;
|
||||
activeRoomLink: string;
|
||||
onRoomCreate: () => void;
|
||||
onRoomDestroy: () => void;
|
||||
setErrorMessage: (message: string) => void;
|
||||
theme: AppState["theme"];
|
||||
}) => {
|
||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
|
||||
if (event.target !== document.activeElement) {
|
||||
event.preventDefault();
|
||||
(event.target as HTMLInputElement).select();
|
||||
}
|
||||
};
|
||||
|
||||
const renderRoomDialog = () => {
|
||||
return (
|
||||
<div className="RoomDialog-modal">
|
||||
{!activeRoomLink && (
|
||||
<>
|
||||
<p>
|
||||
You can invite people to your current scene to collaborate with
|
||||
you
|
||||
</p>
|
||||
<p>
|
||||
🔒 Don't worry, the session uses end-to-end encryption, so
|
||||
whatever you draw will stay private. Not even our server will be
|
||||
able to see what you come up with
|
||||
</p>
|
||||
<div className="RoomDialog-sessionStartButtonContainer">
|
||||
<ToolButton
|
||||
className="RoomDialog-startSession"
|
||||
type="button"
|
||||
icon={start}
|
||||
title="Start Session"
|
||||
aria-label="Start Session"
|
||||
showAriaLabel={true}
|
||||
onClick={onRoomCreate}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeRoomLink && (
|
||||
<>
|
||||
<p>Live-collaboration session is now in progress</p>
|
||||
<p>Share this link with anyone you want to collaborate with:</p>
|
||||
<div className="RoomDialog-linkContainer">
|
||||
<input
|
||||
value={activeRoomLink}
|
||||
readOnly={true}
|
||||
className="RoomDialog-link"
|
||||
ref={roomLinkInput}
|
||||
onPointerDown={selectInput}
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<span role="img" aria-hidden="true" className="RoomDialog-emoji">
|
||||
{"🔒"}
|
||||
</span>
|
||||
Don't worry, the session uses end-to-end encryption, so whatever
|
||||
you draw will stay private. Not even our server will be able to
|
||||
see what you come up with
|
||||
</p>
|
||||
<p>
|
||||
Stopping the session will disconnect you from the room, but you'll
|
||||
be able to continue working with the scene, locally. Note that
|
||||
this won't affect other people, and they'll still be able to
|
||||
collaborate on their version.
|
||||
</p>
|
||||
<div className="RoomDialog-sessionStartButtonContainer">
|
||||
<ToolButton
|
||||
className="RoomDialog-stopSession"
|
||||
type="button"
|
||||
icon={stop}
|
||||
title="Stop session"
|
||||
aria-label="Stop session"
|
||||
showAriaLabel={true}
|
||||
onClick={onRoomDestroy}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
small
|
||||
onCloseRequest={handleClose}
|
||||
title="Live collaboration"
|
||||
theme={theme}
|
||||
>
|
||||
{renderRoomDialog()}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomDialog;
|
||||
@@ -1,74 +0,0 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Dialog {
|
||||
user-select: text;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.Dialog__title {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
grid-template-columns: 1fr calc(var(--space-factor) * 7);
|
||||
grid-gap: var(--metric);
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
text-align: center;
|
||||
font-variant: small-caps;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.Dialog__titleContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Dialog .Modal__close {
|
||||
color: var(--icon-fill-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Dialog__content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
.Dialog {
|
||||
--metric: calc(var(--space-factor) * 4);
|
||||
--inset-left: #{"max(var(--metric), var(--sal))"};
|
||||
--inset-right: #{"max(var(--metric), var(--sar))"};
|
||||
}
|
||||
|
||||
.Dialog__title {
|
||||
grid-template-columns: calc(var(--space-factor) * 7) 1fr calc(
|
||||
var(--space-factor) * 7
|
||||
);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
background: var(--island-bg-color);
|
||||
font-size: 1.25em;
|
||||
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--button-gray-2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.Dialog__titleContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Dialog .Island {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
padding-left: #{"max(calc(var(--padding) * var(--space-factor)), var(--sal))"};
|
||||
padding-right: #{"max(calc(var(--padding) * var(--space-factor)), var(--sar))"};
|
||||
padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), var(--sab))"};
|
||||
}
|
||||
|
||||
.Dialog .Modal__close {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { AppState } from "@excalidraw/excalidraw/types/types";
|
||||
import clsx from "clsx";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { KEYS } from "../keys";
|
||||
import { Island } from "./Island";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
import "./Dialog.scss";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { close } from "./icons";
|
||||
|
||||
export const Dialog = (props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
onCloseRequest(): void;
|
||||
title: React.ReactNode;
|
||||
autofocus?: boolean;
|
||||
theme?: AppState["theme"];
|
||||
}) => {
|
||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||
const [lastActiveElement] = useState(document.activeElement);
|
||||
|
||||
useEffect(() => {
|
||||
if (!islandNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusableElements = queryFocusableElements(islandNode);
|
||||
|
||||
if (focusableElements.length > 0 && props.autofocus !== false) {
|
||||
// If there's an element other than close, focus it.
|
||||
(focusableElements[1] || focusableElements[0]).focus();
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === KEYS.TAB) {
|
||||
const focusableElements = queryFocusableElements(islandNode);
|
||||
const { activeElement } = document;
|
||||
const currentIndex = focusableElements.findIndex(
|
||||
(element) => element === activeElement,
|
||||
);
|
||||
|
||||
if (currentIndex === 0 && event.shiftKey) {
|
||||
focusableElements[focusableElements.length - 1].focus();
|
||||
event.preventDefault();
|
||||
} else if (
|
||||
currentIndex === focusableElements.length - 1 &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
focusableElements[0].focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
islandNode.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||
}, [islandNode, props.autofocus]);
|
||||
|
||||
const queryFocusableElements = (node: HTMLElement) => {
|
||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
||||
"button, a, input, select, textarea, div[tabindex]",
|
||||
);
|
||||
|
||||
return focusableElements ? Array.from(focusableElements) : [];
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
(lastActiveElement as HTMLElement).focus();
|
||||
props.onCloseRequest();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={clsx("Dialog", props.className)}
|
||||
labelledBy="dialog-title"
|
||||
maxWidth={props.small ? 550 : 800}
|
||||
onCloseRequest={onClose}
|
||||
>
|
||||
<Island ref={setIslandNode}>
|
||||
<h2 className="Dialog__title">
|
||||
<span className="Dialog__titleContent">{props.title}</span>
|
||||
<button className="Modal__close" onClick={onClose} aria-label="close">
|
||||
{close}
|
||||
</button>
|
||||
</h2>
|
||||
<div className="Dialog__content">{props.children}</div>
|
||||
</Island>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
.excalidraw {
|
||||
.Island {
|
||||
--padding: 0;
|
||||
background-color: var(--island-bg-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: 4px;
|
||||
padding: calc(var(--padding) * var(--space-factor));
|
||||
position: relative;
|
||||
transition: box-shadow 0.5s ease-in-out;
|
||||
|
||||
&.zen-mode {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import "./Island.scss";
|
||||
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
type IslandProps = {
|
||||
children: React.ReactNode;
|
||||
padding?: number;
|
||||
className?: string | boolean;
|
||||
style?: object;
|
||||
};
|
||||
|
||||
export const Island = React.forwardRef<HTMLDivElement, IslandProps>(
|
||||
({ children, padding, className, style }, ref) => (
|
||||
<div
|
||||
className={clsx("Island", className)}
|
||||
style={{ "--padding": padding, ...style }}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
@@ -1,100 +0,0 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
&.excalidraw-modal-container {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
padding: calc(var(--space-factor) * 10);
|
||||
}
|
||||
|
||||
.Modal__background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
background-color: transparentize($oc-black, 0.3);
|
||||
}
|
||||
|
||||
.Modal__content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
max-height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
animation: Modal__content_fade-in 0.1s ease-out 0.05s forwards;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
// for modals, reset blurry bg
|
||||
background: var(--island-bg-color);
|
||||
|
||||
border: 1px solid var(--dialog-border-color);
|
||||
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes Modal__content_fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.Modal__close {
|
||||
width: calc(var(--space-factor) * 7);
|
||||
height: calc(var(--space-factor) * 7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
height: calc(var(--space-factor) * 5);
|
||||
}
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
.Modal {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.Modal__content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import "./Modal.scss";
|
||||
|
||||
import React, { useState, useLayoutEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import clsx from "clsx";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
export const Modal = (props: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
maxWidth?: number;
|
||||
onCloseRequest(): void;
|
||||
labelledBy: string;
|
||||
}) => {
|
||||
const modalRoot = useBodyRoot();
|
||||
|
||||
if (!modalRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleKeydown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
props.onCloseRequest();
|
||||
}
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={clsx("Modal", props.className)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
onKeyDown={handleKeydown}
|
||||
aria-labelledby={props.labelledBy}
|
||||
>
|
||||
<div className="Modal__background" onClick={props.onCloseRequest}></div>
|
||||
<div
|
||||
className="Modal__content"
|
||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>,
|
||||
modalRoot,
|
||||
);
|
||||
};
|
||||
|
||||
const useBodyRoot = () => {
|
||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
||||
setDiv(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return div;
|
||||
};
|
||||
@@ -1,117 +0,0 @@
|
||||
import "./ToolIcon.scss";
|
||||
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export type ToolButtonSize = "small" | "medium";
|
||||
|
||||
type ToolButtonBaseProps = {
|
||||
icon?: React.ReactNode;
|
||||
"aria-label": string;
|
||||
"aria-keyshortcuts"?: string;
|
||||
"data-testid"?: string;
|
||||
label?: string;
|
||||
title?: string;
|
||||
name?: string;
|
||||
id?: string;
|
||||
size?: ToolButtonSize;
|
||||
keyBindingLabel?: string;
|
||||
showAriaLabel?: boolean;
|
||||
hidden?: boolean;
|
||||
visible?: boolean;
|
||||
selected?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type ToolButtonProps =
|
||||
| (ToolButtonBaseProps & {
|
||||
type: "button";
|
||||
children?: React.ReactNode;
|
||||
onClick?(): void;
|
||||
})
|
||||
| (ToolButtonBaseProps & {
|
||||
type: "icon";
|
||||
children?: React.ReactNode;
|
||||
onClick?(): void;
|
||||
})
|
||||
| (ToolButtonBaseProps & {
|
||||
type: "radio";
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
});
|
||||
|
||||
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||
const innerRef = React.useRef(null);
|
||||
React.useImperativeHandle(ref, () => innerRef.current);
|
||||
const sizeCn = `ToolIcon_size_${props.size}`;
|
||||
|
||||
if (props.type === "button" || props.type === "icon") {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"ToolIcon_type_button",
|
||||
sizeCn,
|
||||
props.className,
|
||||
props.visible && !props.hidden
|
||||
? "ToolIcon_type_button--show"
|
||||
: "ToolIcon_type_button--hide",
|
||||
{
|
||||
ToolIcon: !props.hidden,
|
||||
"ToolIcon--selected": props.selected,
|
||||
"ToolIcon--plain": props.type === "icon",
|
||||
},
|
||||
)}
|
||||
data-testid={props["data-testid"]}
|
||||
hidden={props.hidden}
|
||||
title={props.title}
|
||||
aria-label={props["aria-label"]}
|
||||
type="button"
|
||||
onClick={props.onClick}
|
||||
ref={innerRef}
|
||||
>
|
||||
{(props.icon || props.label) && (
|
||||
<div className="ToolIcon__icon" aria-hidden="true">
|
||||
{props.icon || props.label}
|
||||
{props.keyBindingLabel && (
|
||||
<span className="ToolIcon__keybinding">
|
||||
{props.keyBindingLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{props.showAriaLabel && (
|
||||
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
||||
)}
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={clsx("ToolIcon", props.className)} title={props.title}>
|
||||
<input
|
||||
className={`ToolIcon_type_radio ${sizeCn}`}
|
||||
type="radio"
|
||||
name={props.name}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
||||
data-testid={props["data-testid"]}
|
||||
onChange={props.onChange}
|
||||
checked={props.checked}
|
||||
ref={innerRef}
|
||||
/>
|
||||
<div className="ToolIcon__icon">
|
||||
{props.icon}
|
||||
{props.keyBindingLabel && (
|
||||
<span className="ToolIcon__keybinding">{props.keyBindingLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
ToolButton.defaultProps = {
|
||||
visible: true,
|
||||
className: "",
|
||||
size: "medium",
|
||||
};
|
||||
@@ -1,235 +0,0 @@
|
||||
@import "open-color/open-color.scss";
|
||||
@import "../css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.ToolIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
font-family: Cascadia;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
border-radius: var(--space-factor);
|
||||
user-select: none;
|
||||
|
||||
background-color: var(--button-gray-1);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon--plain {
|
||||
background-color: transparent;
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--icon-fill-color);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border-radius: var(--space-factor);
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
height: 1em;
|
||||
fill: var(--icon-fill-color);
|
||||
color: var(--icon-fill-color);
|
||||
}
|
||||
|
||||
& + .ToolIcon__label {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__label {
|
||||
color: var(--icon-fill-color);
|
||||
font-family: var(--ui-font);
|
||||
margin: 0 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ToolIcon_size_small .ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.excalidraw .ToolIcon_type_button,
|
||||
.Modal .ToolIcon_type_button,
|
||||
.ToolIcon_type_button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||
}
|
||||
|
||||
&.ToolIcon--selected {
|
||||
background-color: var(--button-gray-2);
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
|
||||
&--show {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&--hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon_type_radio,
|
||||
.ToolIcon_type_checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
|
||||
background-color: var(--button-gray-2);
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus + .ToolIcon__icon {
|
||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||
}
|
||||
|
||||
&:active + .ToolIcon__icon {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon_type_floating {
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ToolIcon__icon {
|
||||
background-color: var(--button-gray-1);
|
||||
&:hover {
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
|
||||
width: 2rem;
|
||||
height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
margin-inline-end: var(--space-factor);
|
||||
&.ToolIcon_type_floating {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__keybinding {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 3px;
|
||||
font-size: 0.5em;
|
||||
color: var(--keybinding-color);
|
||||
font-family: var(--ui-font);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// shrink shape icons on small viewports to make them fit
|
||||
@media (max-width: 425px) {
|
||||
.Shape .ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
svg {
|
||||
height: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// move the lock button out of the way on small viewports
|
||||
// it begins to collide with the GitHub icon before we switch to mobile mode
|
||||
@media (max-width: 760px) {
|
||||
.ToolIcon.ToolIcon_type_floating {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
|
||||
margin-left: 0;
|
||||
border-radius: 20px 0 0 20px;
|
||||
z-index: 1;
|
||||
|
||||
background-color: var(--button-gray-1);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-gray-1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
|
||||
.ToolIcon__icon {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
.ToolIcon.ToolIcon__library {
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
margin-inline-end: 0;
|
||||
top: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.unlocked-icon {
|
||||
:root[dir="ltr"] & {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// All icons are imported from https://fontawesome.com/icons?d=gallery
|
||||
// Icons are under the license https://fontawesome.com/license
|
||||
//
|
||||
|
||||
// Note: when adding new icons, review https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/RTL_Guidelines
|
||||
// to determine whether or not the icons should be mirrored in right-to-left languages.
|
||||
|
||||
import React from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
type Opts = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
mirror?: true;
|
||||
} & React.SVGProps<SVGSVGElement>;
|
||||
|
||||
export const createIcon = (
|
||||
d: string | React.ReactNode,
|
||||
opts: number | Opts = 512,
|
||||
) => {
|
||||
const {
|
||||
width = 512,
|
||||
height = width,
|
||||
mirror,
|
||||
style,
|
||||
} = typeof opts === "number" ? ({ width: opts } as Opts) : opts;
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className={clsx({ "rtl-mirror": mirror })}
|
||||
style={style}
|
||||
>
|
||||
{typeof d === "string" ? <path fill="currentColor" d={d} /> : d}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// not mirrored because it's inspired by a playback control, which is always RTL
|
||||
export const start = createIcon(
|
||||
"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm115.7 272l-176 101c-15.8 8.8-35.7-2.5-35.7-21V152c0-18.4 19.8-29.8 35.7-21l176 107c16.4 9.2 16.4 32.9 0 42z",
|
||||
);
|
||||
|
||||
export const stop = createIcon(
|
||||
"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm96 328c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V176c0-8.8 7.2-16 16-16h160c8.8 0 16 7.2 16 16v160z",
|
||||
);
|
||||
|
||||
export const close = createIcon(
|
||||
"M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z",
|
||||
{ width: 352, height: 512 },
|
||||
);
|
||||
@@ -5,3 +5,7 @@ export enum EVENT {
|
||||
|
||||
export const WEBEX_URL =
|
||||
"https://binaries.webex.com/static-content-pipeline/webex-embedded-app/v1/webex-embedded-app-sdk.js";
|
||||
|
||||
export const ENV = {
|
||||
DEVELOPMENT: "development",
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ENV } from "./constants";
|
||||
|
||||
export type ResolvablePromise<T> = Promise<T> & {
|
||||
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
|
||||
reject: (error: Error) => void;
|
||||
@@ -25,3 +27,7 @@ export const loadScript = (filePath: string) => {
|
||||
document.head.append(script);
|
||||
});
|
||||
};
|
||||
|
||||
export const isDev = () => {
|
||||
return process.env.NODE_ENV === ENV.DEVELOPMENT;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user