Skip to content

Commit

Permalink
feat: introducing Web-Embeds (alias iframe element) (excalidraw#6691)
Browse files Browse the repository at this point in the history
Co-authored-by: dwelle <[email protected]>
  • Loading branch information
2 people authored and alswl committed Nov 15, 2023
1 parent 321a269 commit 1813bd4
Show file tree
Hide file tree
Showing 48 changed files with 1,924 additions and 235 deletions.
12 changes: 11 additions & 1 deletion dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ All `props` are *optional*.
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |

### Storing custom data on Excalidraw elements

Expand Down Expand Up @@ -215,7 +217,6 @@ Indicates whether to bind keyboard events to `document`. Disabled by default, me

Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar).


### autoFocus

This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
Expand All @@ -228,3 +229,12 @@ Allows you to override `id` generation for files added on canvas (images). By de
(file: File) => string | Promise<string>
```

### validateEmbeddable

```tsx
validateEmbeddable?: boolean | string[] | RegExp | RegExp[] | ((link: string) => boolean | undefined)
```

This is an optional property. By default we support a handful of well-known sites. You may allow additional sites or disallow the default ones by supplying a custom validator. If you pass `true`, all URLs will be allowed. You can also supply a list of hostnames, RegExp (or list of RegExp objects), or a function. If the function returns `undefined`, the built-in validator will be used.

Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.
13 changes: 13 additions & 0 deletions dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,16 @@ function App() {
);
}
```

## renderEmbeddable

<pre>
(element: NonDeleted&lt;ExcalidrawEmbeddableElement&gt;, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>) => JSX.Element | null
</pre>

Allows you to replace the renderer for embeddable elements (which renders `<iframe>` elements).

| Parameter | Type | Description |
| --- | --- | --- |
| `element` | `NonDeleted<ExcalidrawEmbeddableElement>` | The embeddable element to be rendered. |
| `appState` | `AppState` | The current state of the UI. |
20 changes: 12 additions & 8 deletions src/actions/actionAddToLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { register } from "./register";
import { deepCopyElement } from "../element/newElement";
import { randomId } from "../random";
import { t } from "../i18n";
import { LIBRARY_DISABLED_TYPES } from "../constants";

export const actionAddToLibrary = register({
name: "addToLibrary",
Expand All @@ -12,14 +13,17 @@ export const actionAddToLibrary = register({
includeBoundTextElement: true,
includeElementsInFrames: true,
});
if (selectedElements.some((element) => element.type === "image")) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: "Support for adding images to the library coming soon!",
},
};

for (const type of LIBRARY_DISABLED_TYPES) {
if (selectedElements.some((element) => element.type === type)) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: t(`errors.libraryElementTypeError.${type}`),
},
};
}
}

return app.library
Expand Down
2 changes: 2 additions & 0 deletions src/actions/actionCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ export const actionToggleEraserTool = register({
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
activeTool,
},
commitToHistory: true,
Expand Down Expand Up @@ -430,6 +431,7 @@ export const actionToggleHandTool = register({
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
activeTool,
},
commitToHistory: true,
Expand Down
1 change: 1 addition & 0 deletions src/actions/actionDeleteSelected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const actionDeleteSelected = register({
...nextAppState,
activeTool: updateActiveTool(appState, { type: "selection" }),
multiElement: null,
activeEmbeddable: null,
},
commitToHistory: isSomeElementSelected(
getNonDeletedElements(elements),
Expand Down
1 change: 1 addition & 0 deletions src/actions/actionFinalize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const actionFinalize = register({
multiPointElement
? appState.activeTool
: activeTool,
activeEmbeddable: null,
draggingElement: null,
multiElement: null,
editingElement: null,
Expand Down
1 change: 1 addition & 0 deletions src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type ActionName =
| "removeAllElementsFromFrame"
| "updateFrameRendering"
| "setFrameAsActiveTool"
| "setEmbeddableAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer";

Expand Down
2 changes: 2 additions & 0 deletions src/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const getDefaultAppState = (): Omit<
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
cursorButton: "up",
activeEmbeddable: null,
draggingElement: null,
editingElement: null,
editingGroupId: null,
Expand Down Expand Up @@ -139,6 +140,7 @@ const APP_STATE_STORAGE_CONF = (<
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
activeEmbeddable: { browser: false, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
Expand Down
2 changes: 1 addition & 1 deletion src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type ColorPickerColor =
export type ColorTuple = readonly [string, string, string, string, string];
export type ColorPalette = Merge<
Record<ColorPickerColor, ColorTuple>,
{ black: string; white: string; transparent: string }
{ black: "#1e1e1e"; white: "#ffffff"; transparent: "transparent" }
>;

// used general type instead of specific type (ColorPalette) to support custom colors
Expand Down
114 changes: 82 additions & 32 deletions src/components/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {

import "./Actions.scss";
import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { extraToolsIcon, frameToolIcon } from "./icons";
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
import { KEYS } from "../keys";

export const SelectedShapeActions = ({
Expand Down Expand Up @@ -266,6 +266,7 @@ export const ShapesSwitcher = ({
});
setAppState({
activeTool: nextActiveTool,
activeEmbeddable: null,
multiElement: null,
selectedElementIds: {},
});
Expand All @@ -283,39 +284,72 @@ export const ShapesSwitcher = ({
<div className="App-toolbar__divider" />
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
{device.isMobile ? (
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={frameToolIcon}
checked={activeTool.type === "frame"}
name="editor-current-shape"
title={`${capitalizeString(
t("toolBar.frame"),
)}${KEYS.F.toLocaleUpperCase()}`}
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
aria-label={capitalizeString(t("toolBar.frame"))}
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
data-testid={`toolbar-frame`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
<>
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={frameToolIcon}
checked={activeTool.type === "frame"}
name="editor-current-shape"
title={`${capitalizeString(
t("toolBar.frame"),
)}${KEYS.F.toLocaleUpperCase()}`}
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
aria-label={capitalizeString(t("toolBar.frame"))}
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
data-testid={`toolbar-frame`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
penDetected: true,
penMode: true,
});
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "frame", "ui");
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setAppState({
penDetected: true,
penMode: true,
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
activeEmbeddable: null,
});
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "frame", "ui");
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
/>
}}
/>
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={EmbedIcon}
checked={activeTool.type === "embeddable"}
name="editor-current-shape"
title={capitalizeString(t("toolBar.embeddable"))}
aria-label={capitalizeString(t("toolBar.embeddable"))}
data-testid={`toolbar-embeddable`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
penDetected: true,
penMode: true,
});
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "embeddable", "ui");
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
activeEmbeddable: null,
});
}}
/>
</>
) : (
<DropdownMenu open={isExtraToolsMenuOpen}>
<DropdownMenu.Trigger
Expand Down Expand Up @@ -347,6 +381,22 @@ export const ShapesSwitcher = ({
>
{t("toolBar.frame")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
icon={EmbedIcon}
data-testid="toolbar-embeddable"
>
{t("toolBar.embeddable")}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
)}
Expand Down
Loading

0 comments on commit 1813bd4

Please sign in to comment.