1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2025-02-18 13:29:36 +01:00

feat: remove ExcalidrawEmbeddableElement.validated flag (#7539)

This commit is contained in:
David Luzar 2024-01-11 17:42:51 +01:00 committed by GitHub
parent 86cfeb714c
commit 0c24a7042f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 94 additions and 39 deletions

@ -14,9 +14,12 @@ Please add the latest change on the top under the correct section.
## Unreleased
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
- Remove `ExcalidrawEmbeddableElement.validated` attribute. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
### Breaking Changes
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
- Create an `ESM` build for `@excalidraw/excalidraw`. The API is in progress and subject to change before stable release. There are some changes on how the package will be consumed
#### Bundler
@ -265,7 +268,7 @@ define: {
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)

@ -246,6 +246,7 @@ import {
ToolType,
OnUserFollowedPayload,
UnsubscribeCallback,
EmbedsValidationStatus,
ElementsPendingErasure,
} from "../types";
import {
@ -529,6 +530,15 @@ class App extends React.Component<AppProps, AppState> {
public files: BinaryFiles = {};
public imageCache: AppClassProperties["imageCache"] = new Map();
private iFrameRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement>();
/**
* Indicates whether the embeddable's url has been validated for rendering.
* If value not set, indicates that the validation is pending.
* Initially or on url change the flag is not reset so that we can guarantee
* the validation came from a trusted source (the editor).
**/
private embedsValidationStatus: EmbedsValidationStatus = new Map();
/** embeds that have been inserted to DOM (as a perf optim, we don't want to
* insert to DOM before user initially scrolls to them) */
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>();
private elementsPendingErasure: ElementsPendingErasure = new Set();
@ -869,6 +879,14 @@ class App extends React.Component<AppProps, AppState> {
);
}
private updateEmbedValidationStatus = (
element: ExcalidrawEmbeddableElement,
status: boolean,
) => {
this.embedsValidationStatus.set(element.id, status);
ShapeCache.delete(element);
};
private updateEmbeddables = () => {
const iframeLikes = new Set<ExcalidrawIframeLikeElement["id"]>();
@ -876,7 +894,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements().filter((element) => {
if (isEmbeddableElement(element)) {
iframeLikes.add(element.id);
if (element.validated == null) {
if (!this.embedsValidationStatus.has(element.id)) {
updated = true;
const validated = embeddableURLValidator(
@ -884,8 +902,7 @@ class App extends React.Component<AppProps, AppState> {
this.props.validateEmbeddable,
);
mutateElement(element, { validated }, false);
ShapeCache.delete(element);
this.updateEmbedValidationStatus(element, validated);
}
} else if (isIframeElement(element)) {
iframeLikes.add(element.id);
@ -914,7 +931,9 @@ class App extends React.Component<AppProps, AppState> {
.getNonDeletedElements()
.filter(
(el): el is NonDeleted<ExcalidrawIframeLikeElement> =>
(isEmbeddableElement(el) && !!el.validated) || isIframeElement(el),
(isEmbeddableElement(el) &&
this.embedsValidationStatus.get(el.id) === true) ||
isIframeElement(el),
);
return (
@ -1507,6 +1526,9 @@ class App extends React.Component<AppProps, AppState> {
setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen}
setToast={this.setToast}
updateEmbedValidationStatus={
this.updateEmbedValidationStatus
}
/>
)}
{this.props.aiEnabled !== false &&
@ -1617,6 +1639,7 @@ class App extends React.Component<AppProps, AppState> {
renderGrid: true,
canvasBackgroundColor:
this.state.viewBackgroundColor,
embedsValidationStatus: this.embedsValidationStatus,
elementsPendingErasure: this.elementsPendingErasure,
}}
/>
@ -6425,7 +6448,6 @@ class App extends React.Component<AppProps, AppState> {
width: embedLink.intrinsicSize.w,
height: embedLink.intrinsicSize.h,
link,
validated: null,
});
this.scene.replaceAllElements([
@ -6659,7 +6681,6 @@ class App extends React.Component<AppProps, AppState> {
if (elementType === "embeddable") {
element = newEmbeddableElement({
type: "embeddable",
validated: null,
...baseElementAttributes,
});
} else {

@ -295,11 +295,8 @@ const restoreElement = (
case "rectangle":
case "diamond":
case "iframe":
return restoreElementWithProperties(element, {});
case "embeddable":
return restoreElementWithProperties(element, {
validated: null,
});
return restoreElementWithProperties(element, {});
case "magicframe":
case "frame":
return restoreElementWithProperties(element, {

@ -39,7 +39,6 @@ import "./Hyperlink.scss";
import { trackEvent } from "../analytics";
import { useAppProps, useExcalidrawAppState } from "../components/App";
import { isEmbeddableElement } from "./typeChecks";
import { ShapeCache } from "../scene/ShapeCache";
const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85;
@ -64,6 +63,7 @@ export const Hyperlink = ({
setAppState,
onLinkOpen,
setToast,
updateEmbedValidationStatus,
}: {
element: NonDeletedExcalidrawElement;
setAppState: React.Component<any, AppState>["setState"];
@ -71,6 +71,10 @@ export const Hyperlink = ({
setToast: (
toast: { message: string; closable?: boolean; duration?: number } | null,
) => void;
updateEmbedValidationStatus: (
element: ExcalidrawEmbeddableElement,
status: boolean,
) => void;
}) => {
const appState = useExcalidrawAppState();
const appProps = useAppProps();
@ -98,9 +102,9 @@ export const Hyperlink = ({
}
if (!link) {
mutateElement(element, {
validated: false,
link: null,
});
updateEmbedValidationStatus(element, false);
return;
}
@ -110,10 +114,9 @@ export const Hyperlink = ({
}
element.link && embeddableLinkCache.set(element.id, element.link);
mutateElement(element, {
validated: false,
link,
});
ShapeCache.delete(element);
updateEmbedValidationStatus(element, false);
} else {
const { width, height } = element;
const embedLink = getEmbedLink(link);
@ -142,10 +145,9 @@ export const Hyperlink = ({
: height,
}
: {}),
validated: true,
link,
});
ShapeCache.delete(element);
updateEmbedValidationStatus(element, true);
if (embeddableLinkCache.has(element.id)) {
embeddableLinkCache.delete(element.id);
}
@ -159,6 +161,7 @@ export const Hyperlink = ({
appProps.validateEmbeddable,
appState.activeEmbeddable,
setAppState,
updateEmbedValidationStatus,
]);
useLayoutEffect(() => {

@ -136,13 +136,9 @@ export const newElement = (
export const newEmbeddableElement = (
opts: {
type: "embeddable";
validated: ExcalidrawEmbeddableElement["validated"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawEmbeddableElement> => {
return {
..._newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts),
validated: opts.validated,
};
return _newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts);
};
export const newIframeElement = (

@ -88,14 +88,6 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
Readonly<{
type: "embeddable";
/**
* indicates whether the embeddable src (url) has been validated for rendering.
* null value indicates that the validation is pending. We reset the
* value on each restore (or url change) so that we can guarantee
* the validation came from a trusted source (the editor). Also because we
* may not have access to host-app supplied url validator during restore.
*/
validated: boolean | null;
}>;
export type ExcalidrawIframeElement = _ExcalidrawElementBase &

@ -1007,7 +1007,9 @@ const _renderStaticScene = ({
if (
isIframeLikeElement(element) &&
(isExporting ||
(isEmbeddableElement(element) && !element.validated)) &&
(isEmbeddableElement(element) &&
renderConfig.embedsValidationStatus.get(element.id) !==
true)) &&
element.width &&
element.height
) {

@ -21,6 +21,7 @@ import {
isLinearElement,
} from "../element/typeChecks";
import { canChangeRoundness } from "./comparisons";
import { EmbedsValidationStatus } from "../types";
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
@ -118,10 +119,13 @@ export const generateRoughOptions = (
const modifyIframeLikeForRoughOptions = (
element: NonDeletedExcalidrawElement,
isExporting: boolean,
embedsValidationStatus: EmbedsValidationStatus | null,
) => {
if (
isIframeLikeElement(element) &&
(isExporting || (isEmbeddableElement(element) && !element.validated)) &&
(isExporting ||
(isEmbeddableElement(element) &&
embedsValidationStatus?.get(element.id) !== true)) &&
isTransparent(element.backgroundColor) &&
isTransparent(element.strokeColor)
) {
@ -278,7 +282,12 @@ export const _generateElementShape = (
{
isExporting,
canvasBackgroundColor,
}: { isExporting: boolean; canvasBackgroundColor: string },
embedsValidationStatus,
}: {
isExporting: boolean;
canvasBackgroundColor: string;
embedsValidationStatus: EmbedsValidationStatus | null;
},
): Drawable | Drawable[] | null => {
switch (element.type) {
case "rectangle":
@ -299,7 +308,11 @@ export const _generateElementShape = (
h - r
} L 0 ${r} Q 0 0, ${r} 0`,
generateRoughOptions(
modifyIframeLikeForRoughOptions(element, isExporting),
modifyIframeLikeForRoughOptions(
element,
isExporting,
embedsValidationStatus,
),
true,
),
);
@ -310,7 +323,11 @@ export const _generateElementShape = (
element.width,
element.height,
generateRoughOptions(
modifyIframeLikeForRoughOptions(element, isExporting),
modifyIframeLikeForRoughOptions(
element,
isExporting,
embedsValidationStatus,
),
false,
),
);

@ -8,7 +8,7 @@ import { elementWithCanvasCache } from "../renderer/renderElement";
import { _generateElementShape } from "./Shape";
import { ElementShape, ElementShapes } from "./types";
import { COLOR_PALETTE } from "../colors";
import { AppState } from "../types";
import { AppState, EmbedsValidationStatus } from "../types";
export class ShapeCache {
private static rg = new RoughGenerator();
@ -51,6 +51,7 @@ export class ShapeCache {
renderConfig: {
isExporting: boolean;
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
} | null,
) => {
// when exporting, always regenerated to guarantee the latest shape
@ -72,6 +73,7 @@ export class ShapeCache {
renderConfig || {
isExporting: false,
canvasBackgroundColor: COLOR_PALETTE.white,
embedsValidationStatus: null,
},
) as T["type"] extends keyof ElementShapes
? ElementShapes[T["type"]]

@ -266,6 +266,8 @@ export const exportToCanvas = async (
imageCache,
renderGrid: false,
isExporting: true,
// empty disables embeddable rendering
embedsValidationStatus: new Map(),
elementsPendingErasure: new Set(),
},
});
@ -288,6 +290,9 @@ export const exportToSvg = async (
},
files: BinaryFiles | null,
opts?: {
/**
* if true, all embeddables passed in will be rendered when possible.
*/
renderEmbeddables?: boolean;
exportingFrame?: ExcalidrawFrameLikeElement | null;
},
@ -428,14 +433,24 @@ export const exportToSvg = async (
}
const rsvg = rough.svg(svgRoot);
const renderEmbeddables = opts?.renderEmbeddables ?? false;
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
offsetX,
offsetY,
isExporting: true,
exportWithDarkMode,
renderEmbeddables: opts?.renderEmbeddables ?? false,
renderEmbeddables,
frameRendering,
canvasBackgroundColor: viewBackgroundColor,
embedsValidationStatus: renderEmbeddables
? new Map(
elementsForRender
.filter((element) => isFrameLikeElement(element))
.map((element) => [element.id, true]),
)
: new Map(),
});
tempScene.destroy();

@ -7,6 +7,7 @@ import {
import {
AppClassProperties,
AppState,
EmbedsValidationStatus,
ElementsPendingErasure,
InteractiveCanvasAppState,
StaticCanvasAppState,
@ -21,6 +22,7 @@ export type StaticCanvasRenderConfig = {
/** when exporting the behavior is slightly different (e.g. we can't use
CSS filters), and we disable render optimizations for best output */
isExporting: boolean;
embedsValidationStatus: EmbedsValidationStatus;
elementsPendingErasure: ElementsPendingErasure;
};
@ -32,6 +34,7 @@ export type SVGRenderConfig = {
renderEmbeddables: boolean;
frameRendering: AppState["frameRendering"];
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
};
export type InteractiveCanvasRenderConfig = {

@ -34,7 +34,6 @@ export const rectangleFixture: ExcalidrawElement = {
export const embeddableFixture: ExcalidrawElement = {
...elementBase,
type: "embeddable",
validated: null,
};
export const ellipseFixture: ExcalidrawElement = {
...elementBase,

@ -205,7 +205,6 @@ export class API {
element = newEmbeddableElement({
type: "embeddable",
...base,
validated: null,
});
break;
case "iframe":

@ -19,6 +19,7 @@ import {
ExcalidrawMagicFrameElement,
ExcalidrawFrameLikeElement,
ExcalidrawElementType,
ExcalidrawIframeLikeElement,
} from "./element/types";
import { Action } from "./actions/types";
import { Point as RoughPoint } from "roughjs/bin/geometry";
@ -746,4 +747,9 @@ export type Primitive =
export type JSONValue = string | number | boolean | null | object;
export type EmbedsValidationStatus = Map<
ExcalidrawIframeLikeElement["id"],
boolean
>;
export type ElementsPendingErasure = Set<ExcalidrawElement["id"]>;