From 28ab6531c92c9e51f08ac3b64a9c4f6d24058e4f Mon Sep 17 00:00:00 2001 From: Sudharsan Aravind <63002244+vsaravind01@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:58:11 +0530 Subject: [PATCH 01/48] fix: updated link for documentation page under help section (#6654) * fix: updated link for documentation page under help section * Update docs link --------- Co-authored-by: Aakansha Doshi --- src/components/HelpDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index e78b2625f..b51fadfca 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -12,7 +12,7 @@ const Header = () => (
From 6de6a96abf29b5990d50316cc95fa80766db95e5 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 16 Jun 2023 20:55:33 +0530 Subject: [PATCH 02/48] docs: add info about roadmap (#6687) --- dev-docs/docs/introduction/contributing.mdx | 5 +++++ src/packages/excalidraw/README.md | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/dev-docs/docs/introduction/contributing.mdx b/dev-docs/docs/introduction/contributing.mdx index d384c97a1..821355e0e 100644 --- a/dev-docs/docs/introduction/contributing.mdx +++ b/dev-docs/docs/introduction/contributing.mdx @@ -2,6 +2,11 @@ Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change. +We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you. +For new contributors we would recommend to start with *Easy* tasks. + +In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it. + ## Setup ### Option 1 - Manual diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index d650885df..3e2f8efc6 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -43,3 +43,7 @@ Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/ ## API Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api) + +## Contributing + +Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/contributing) From 7f7128ec09906882076a57a792eb515403f2653d Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 19 Jun 2023 13:47:28 +0530 Subject: [PATCH 03/48] fix: don't allow binding text to images (#6693) --- src/element/textElement.ts | 1 - src/element/typeChecks.test.ts | 18 +++++++++--------- src/element/typeChecks.ts | 1 - src/element/types.ts | 1 - 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 3956393bc..76de2c886 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -862,7 +862,6 @@ const VALID_CONTAINER_TYPES = new Set([ "rectangle", "ellipse", "diamond", - "image", "arrow", ]); diff --git a/src/element/typeChecks.test.ts b/src/element/typeChecks.test.ts index ab04c29b7..60eb9e273 100644 --- a/src/element/typeChecks.test.ts +++ b/src/element/typeChecks.test.ts @@ -30,15 +30,6 @@ describe("Test TypeChecks", () => { }), ), ).toBeTruthy(); - - expect( - hasBoundTextElement( - API.createElement({ - type: "image", - boundElements: [{ type: "text", id: "text-id" }], - }), - ), - ).toBeTruthy(); }); it("should return false for text bindable containers without bound text", () => { @@ -62,5 +53,14 @@ describe("Test TypeChecks", () => { ), ).toBeFalsy(); }); + + expect( + hasBoundTextElement( + API.createElement({ + type: "image", + boundElements: [{ type: "text", id: "text-id" }], + }), + ), + ).toBeFalsy(); }); }); diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts index 302e9d375..d255a9a25 100644 --- a/src/element/typeChecks.ts +++ b/src/element/typeChecks.ts @@ -126,7 +126,6 @@ export const isTextBindableContainer = ( (element.type === "rectangle" || element.type === "diamond" || element.type === "ellipse" || - element.type === "image" || isArrowElement(element)) ); }; diff --git a/src/element/types.ts b/src/element/types.ts index ad4610160..a8c49e1a0 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -162,7 +162,6 @@ export type ExcalidrawTextContainer = | ExcalidrawRectangleElement | ExcalidrawDiamondElement | ExcalidrawEllipseElement - | ExcalidrawImageElement | ExcalidrawArrowElement; export type ExcalidrawTextElementWithContainer = { From 0aa1e664867dd375767fba4d222988fbffdcec45 Mon Sep 17 00:00:00 2001 From: Milos Vetesnik Date: Mon, 19 Jun 2023 11:18:28 +0200 Subject: [PATCH 04/48] feat: simple analitycs (#6683) * Simple analytics for iframe and webpage * added logic for tracking specific categories of events to reduce it * enviroment vars clean up * fix: lint for index.html --- .env.development | 6 +--- .env.production | 11 +------- public/index.html | 71 ++++++++++++++++++++++------------------------- src/analytics.ts | 19 ++++--------- 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/.env.development b/.env.development index 0c2fb5527..c56b62b36 100644 --- a/.env.development +++ b/.env.development @@ -20,14 +20,10 @@ REACT_APP_DEV_ENABLE_SW= # whether to disable live reload / HMR. Usuaully what you want to do when # debugging Service Workers. REACT_APP_DEV_DISABLE_LIVE_RELOAD= +REACT_APP_DISABLE_TRACKING=true FAST_REFRESH=false -# MATOMO -REACT_APP_MATOMO_URL= -REACT_APP_CDN_MATOMO_TRACKER_URL= -REACT_APP_MATOMO_SITE_ID= - #Debug flags # To enable bounding box for text containers diff --git a/.env.production b/.env.production index 8737c63c7..b86aa4bcc 100644 --- a/.env.production +++ b/.env.production @@ -11,14 +11,5 @@ REACT_APP_WS_SERVER_URL= REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' -# production-only vars -# GOOGLE ANALYTICS -REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13 -# MATOMO -REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/ -REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js -REACT_APP_MATOMO_SITE_ID=1 - - - REACT_APP_PLUS_APP=https://app.excalidraw.com +REACT_APP_DISABLE_TRACKING= diff --git a/public/index.html b/public/index.html index f65e481f3..5509ded86 100644 --- a/public/index.html +++ b/public/index.html @@ -148,33 +148,6 @@ // setting this so that libraries installation reuses this window tab. window.name = "_excalidraw"; - <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %> - - - - - - - <% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> - - - <% } %> - - <% } %> + `), + aspectRatio: { w: 550, h: 720 }, + }; + } + embeddedLinkCache.set(link, ret); + return ret; + } + + embeddedLinkCache.set(link, { link, aspectRatio, type }); + return { link, aspectRatio, type }; +}; + +export const isEmbeddableOrFrameLabel = ( + element: NonDeletedExcalidrawElement, +): Boolean => { + if (isEmbeddableElement(element)) { + return true; + } + if (element.type === "text") { + const container = getContainerElement(element); + if (container && isEmbeddableElement(container)) { + return true; + } + } + return false; +}; + +export const createPlaceholderEmbeddableLabel = ( + element: ExcalidrawEmbeddableElement, +): ExcalidrawElement => { + const text = + !element.link || element?.link === "" ? "Empty Web-Embed" : element.link; + const fontSize = Math.max( + Math.min(element.width / 2, element.width / text.length), + element.width / 30, + ); + const fontFamily = FONT_FAMILY.Helvetica; + + const fontString = getFontString({ + fontSize, + fontFamily, + }); + + return newTextElement({ + x: element.x + element.width / 2, + y: element.y + element.height / 2, + strokeColor: + element.strokeColor !== "transparent" ? element.strokeColor : "black", + backgroundColor: "transparent", + fontFamily, + fontSize, + text: wrapText(text, fontString, element.width - 20), + textAlign: "center", + verticalAlign: VERTICAL_ALIGN.MIDDLE, + angle: element.angle ?? 0, + }); +}; + +export const actionSetEmbeddableAsActiveTool = register({ + name: "setEmbeddableAsActiveTool", + trackEvent: { category: "toolbar" }, + perform: (elements, appState, _, app) => { + const nextActiveTool = updateActiveTool(appState, { + type: "embeddable", + }); + + setCursorForShape(app.canvas, { + ...appState, + activeTool: nextActiveTool, + }); + + return { + elements, + appState: { + ...appState, + activeTool: updateActiveTool(appState, { + type: "embeddable", + }), + }, + commitToHistory: false, + }; + }, +}); + +const validateHostname = ( + url: string, + /** using a Set assumes it already contains normalized bare domains */ + allowedHostnames: Set | string, +): boolean => { + try { + const { hostname } = new URL(url); + + const bareDomain = hostname.replace(/^www\./, ""); + + if (allowedHostnames instanceof Set) { + return ALLOWED_DOMAINS.has(bareDomain); + } + + if (bareDomain === allowedHostnames.replace(/^www\./, "")) { + return true; + } + } catch (error) { + // ignore + } + return false; +}; + +export const extractSrc = (htmlString: string): string => { + const twitterMatch = htmlString.match(RE_TWITTER_EMBED); + if (twitterMatch && twitterMatch.length === 2) { + return twitterMatch[1]; + } + + const gistMatch = htmlString.match(RE_GH_GIST_EMBED); + if (gistMatch && gistMatch.length === 2) { + return gistMatch[1]; + } + + const match = htmlString.match(RE_GENERIC_EMBED); + if (match && match.length === 2) { + return match[1]; + } + return htmlString; +}; + +export const embeddableURLValidator = ( + url: string | null | undefined, + validateEmbeddable: ExcalidrawProps["validateEmbeddable"], +): boolean => { + if (!url) { + return false; + } + if (validateEmbeddable != null) { + if (typeof validateEmbeddable === "function") { + const ret = validateEmbeddable(url); + // if return value is undefined, leave validation to default + if (typeof ret === "boolean") { + return ret; + } + } else if (typeof validateEmbeddable === "boolean") { + return validateEmbeddable; + } else if (validateEmbeddable instanceof RegExp) { + return validateEmbeddable.test(url); + } else if (Array.isArray(validateEmbeddable)) { + for (const domain of validateEmbeddable) { + if (domain instanceof RegExp) { + if (url.match(domain)) { + return true; + } + } else if (validateHostname(url, domain)) { + return true; + } + } + return false; + } + } + + return validateHostname(url, ALLOWED_DOMAINS); +}; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 848a6af7a..d84261fc2 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -13,6 +13,7 @@ import { FontFamilyValues, ExcalidrawTextContainer, ExcalidrawFrameElement, + ExcalidrawEmbeddableElement, } from "../element/types"; import { arrayToMap, @@ -130,6 +131,18 @@ export const newElement = ( ): NonDeleted => _newElementBase(opts.type, opts); +export const newEmbeddableElement = ( + opts: { + type: "embeddable"; + validated: boolean | undefined; + } & ElementConstructorOpts, +): NonDeleted => { + return { + ..._newElementBase("embeddable", opts), + validated: opts.validated, + }; +}; + export const newFrameElement = ( opts: ElementConstructorOpts, ): NonDeleted => { @@ -177,7 +190,6 @@ export const newTextElement = ( containerId?: ExcalidrawTextContainer["id"]; lineHeight?: ExcalidrawTextElement["lineHeight"]; strokeWidth?: ExcalidrawTextElement["strokeWidth"]; - isFrameName?: boolean; } & ElementConstructorOpts, ): NonDeleted => { const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY; @@ -212,7 +224,6 @@ export const newTextElement = ( containerId: opts.containerId || null, originalText: text, lineHeight, - isFrameName: opts.isFrameName || false, }, {}, ); diff --git a/src/element/showSelectedShapeActions.ts b/src/element/showSelectedShapeActions.ts index e04802934..cc42ec8d0 100644 --- a/src/element/showSelectedShapeActions.ts +++ b/src/element/showSelectedShapeActions.ts @@ -7,11 +7,11 @@ export const showSelectedShapeActions = ( elements: readonly NonDeletedExcalidrawElement[], ) => Boolean( - (!appState.viewModeEnabled && - appState.activeTool.type !== "custom" && - (appState.editingElement || - (appState.activeTool.type !== "selection" && - appState.activeTool.type !== "eraser" && - appState.activeTool.type !== "hand"))) || - getSelectedElements(elements, appState).length, + !appState.viewModeEnabled && + ((appState.activeTool.type !== "custom" && + (appState.editingElement || + (appState.activeTool.type !== "selection" && + appState.activeTool.type !== "eraser" && + appState.activeTool.type !== "hand"))) || + getSelectedElements(elements, appState).length), ); diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts index d255a9a25..3f61e3fc9 100644 --- a/src/element/typeChecks.ts +++ b/src/element/typeChecks.ts @@ -4,6 +4,7 @@ import { MarkNonNullable } from "../utility-types"; import { ExcalidrawElement, ExcalidrawTextElement, + ExcalidrawEmbeddableElement, ExcalidrawLinearElement, ExcalidrawBindableElement, ExcalidrawGenericElement, @@ -24,7 +25,8 @@ export const isGenericElement = ( (element.type === "selection" || element.type === "rectangle" || element.type === "diamond" || - element.type === "ellipse") + element.type === "ellipse" || + element.type === "embeddable") ); }; @@ -40,6 +42,12 @@ export const isImageElement = ( return !!element && element.type === "image"; }; +export const isEmbeddableElement = ( + element: ExcalidrawElement | null | undefined, +): element is ExcalidrawEmbeddableElement => { + return !!element && element.type === "embeddable"; +}; + export const isTextElement = ( element: ExcalidrawElement | null, ): element is ExcalidrawTextElement => { @@ -112,6 +120,7 @@ export const isBindableElement = ( element.type === "diamond" || element.type === "ellipse" || element.type === "image" || + element.type === "embeddable" || (element.type === "text" && !element.containerId)) ); }; @@ -135,6 +144,7 @@ export const isExcalidrawElement = (element: any): boolean => { element?.type === "text" || element?.type === "diamond" || element?.type === "rectangle" || + element?.type === "embeddable" || element?.type === "ellipse" || element?.type === "arrow" || element?.type === "freedraw" || @@ -162,7 +172,8 @@ export const isBoundToContainer = ( ); }; -export const isUsingAdaptiveRadius = (type: string) => type === "rectangle"; +export const isUsingAdaptiveRadius = (type: string) => + type === "rectangle" || type === "embeddable"; export const isUsingProportionalRadius = (type: string) => type === "line" || type === "arrow" || type === "diamond"; @@ -193,17 +204,13 @@ export const canApplyRoundnessTypeToElement = ( export const getDefaultRoundnessTypeForElement = ( element: ExcalidrawElement, ) => { - if ( - element.type === "arrow" || - element.type === "line" || - element.type === "diamond" - ) { + if (isUsingProportionalRadius(element.type)) { return { type: ROUNDNESS.PROPORTIONAL_RADIUS, }; } - if (element.type === "rectangle") { + if (isUsingAdaptiveRadius(element.type)) { return { type: ROUNDNESS.ADAPTIVE_RADIUS, }; diff --git a/src/element/types.ts b/src/element/types.ts index a8c49e1a0..7d799f234 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -84,6 +84,19 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & { type: "ellipse"; }; +export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase & + Readonly<{ + /** + * indicates whether the embeddable src (url) has been validated for rendering. + * nullish 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; + type: "embeddable"; + }>; + export type ExcalidrawImageElement = _ExcalidrawElementBase & Readonly<{ type: "image"; @@ -110,6 +123,7 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & { export type ExcalidrawGenericElement = | ExcalidrawSelectionElement | ExcalidrawRectangleElement + | ExcalidrawEmbeddableElement | ExcalidrawDiamondElement | ExcalidrawEllipseElement; @@ -156,6 +170,7 @@ export type ExcalidrawBindableElement = | ExcalidrawEllipseElement | ExcalidrawTextElement | ExcalidrawImageElement + | ExcalidrawEmbeddableElement | ExcalidrawFrameElement; export type ExcalidrawTextContainer = diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 860437f3c..cd719e276 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -100,6 +100,20 @@ polyfill(); window.EXCALIDRAW_THROTTLE_RENDER = true; +let isSelfEmbedding = false; + +if (window.self !== window.top) { + try { + const parentUrl = new URL(document.referrer); + const currentUrl = new URL(window.location.href); + if (parentUrl.origin === currentUrl.origin) { + isSelfEmbedding = true; + } + } catch (error) { + // ignore + } +} + const languageDetector = new LanguageDetector(); languageDetector.init({ languageUtils: {}, @@ -518,7 +532,9 @@ const ExcalidrawWrapper = () => { const [theme, setTheme] = useState( () => - localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) || + (localStorage.getItem( + STORAGE_KEYS.LOCAL_STORAGE_THEME, + ) as Theme | null) || // FIXME migration from old LS scheme. Can be removed later. #5660 importFromLocalStorage().appState?.theme || THEME.LIGHT, @@ -641,6 +657,25 @@ const ExcalidrawWrapper = () => { const isOffline = useAtomValue(isOfflineAtom); + // browsers generally prevent infinite self-embedding, there are + // cases where it still happens, and while we disallow self-embedding + // by not whitelisting our own origin, this serves as an additional guard + if (isSelfEmbedding) { + return ( +
+

I'm not a pretzel!

+
+ ); + } + return (
{ viewBackgroundColor: COLOR_PALETTE.white, }, files: null, + renderEmbeddables: false, }); }; diff --git a/src/keys.ts b/src/keys.ts index 33f1188dc..da3e6b49d 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -65,6 +65,7 @@ export const KEYS = { Y: "y", Z: "z", K: "k", + W: "w", 0: "0", 1: "1", diff --git a/src/locales/en.json b/src/locales/en.json index 7092e9be8..938535c5e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -109,8 +109,12 @@ "createContainerFromText": "Wrap text in a container", "link": { "edit": "Edit link", + "editEmbed": "Edit link & embed", "create": "Create link", - "label": "Link" + "createEmbed": "Create link & embed", + "label": "Link", + "labelEmbed": "Link & embed", + "empty": "No link is set" }, "lineEditor": { "edit": "Edit line", @@ -164,9 +168,11 @@ "cancel": "Cancel", "clear": "Clear", "remove": "Remove", + "embed": "Toggle embedding", "publishLibrary": "Publish", "submit": "Submit", - "confirm": "Confirm" + "confirm": "Confirm", + "embeddableInteractionButton": "Click to interact" }, "alerts": { "clearReset": "This will clear the whole canvas. Are you sure?", @@ -206,6 +212,10 @@ "line2": "This could result in breaking the Text Elements in your drawings.", "line3": "We strongly recommend disabling this setting. You can follow these steps on how to do so.", "line4": "If disabling this setting doesn't fix the display of text elements, please open an issue on our GitHub, or write us on Discord" + }, + "libraryElementTypeError": { + "embeddable": "Embeddable elements cannot be added to the library.", + "image": "Support for adding images to the library coming soon!" } }, "toolBar": { @@ -224,6 +234,7 @@ "link": "Add/ Update link for a selected shape", "eraser": "Eraser", "frame": "Frame tool", + "embeddable": "Web Embed", "hand": "Hand (panning tool)", "extraTools": "More tools" }, @@ -237,6 +248,7 @@ "linearElement": "Click to start multiple points, drag for single line", "freeDraw": "Click and drag, release when you're finished", "text": "Tip: you can also add text by double-clicking anywhere with the selection tool", + "embeddable": "Click-drag to create a website embed", "text_selected": "Double-click or press ENTER to edit text", "text_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing", "linearElementMulti": "Click on last point or press Escape or Enter to finish", @@ -411,7 +423,9 @@ "fileSavedToFilename": "Saved to {filename}", "canvas": "canvas", "selection": "selection", - "pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor" + "pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor", + "unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted", + "unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site" }, "colors": { "transparent": "Transparent", diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 5933f5649..4d06ecc9b 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -13,8 +13,27 @@ Please add the latest change on the top under the correct section. ## Unreleased +### renderEmbeddable + +```tsx +(element: NonDeletedExcalidrawElement, radius: number, appState: UIAppState) => JSX.Element | null;` +``` + +The renderEmbeddable function allows you to customize the rendering of a JSX component instead of using the default `