feat: add onLinkOpen
component prop (#4694)
Co-authored-by: ad1992 <aakansha1216@gmail.com>
This commit is contained in:
parent
050bc1ce2b
commit
a066317d3c
@ -52,7 +52,7 @@
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.47.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"typescript": "4.5.4"
|
||||
"typescript": "4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@excalidraw/eslint-config": "1.0.0",
|
||||
@ -74,7 +74,7 @@
|
||||
"rewire": "5.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/typescript-estree": "5.3.0"
|
||||
"@typescript-eslint/typescript-estree": "5.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
@ -213,6 +213,7 @@ import {
|
||||
tupleToCoors,
|
||||
viewportCoordsToSceneCoords,
|
||||
withBatchedUpdates,
|
||||
wrapEvent,
|
||||
withBatchedUpdatesThrottled,
|
||||
} from "../utils";
|
||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||
@ -526,6 +527,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element={selectedElement[0]}
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
onLinkOpen={this.props.onLinkOpen}
|
||||
/>
|
||||
)}
|
||||
{this.state.showStats && (
|
||||
@ -2384,8 +2386,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
private redirectToLink = () => {
|
||||
private redirectToLink = (event: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
if (
|
||||
!this.hitLinkElement ||
|
||||
this.lastPointerDown!.clientX !== this.lastPointerUp!.clientX ||
|
||||
this.lastPointerDown!.clientY !== this.lastPointerUp!.clientY
|
||||
) {
|
||||
@ -2412,14 +2415,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.isMobile,
|
||||
);
|
||||
if (lastPointerDownHittingLinkIcon && LastPointerUpHittingLinkIcon) {
|
||||
const url = this.hitLinkElement?.link;
|
||||
const url = this.hitLinkElement.link;
|
||||
if (url) {
|
||||
const target = isLocalLink(url) ? "_self" : "_blank";
|
||||
const newWindow = window.open(undefined, target);
|
||||
// https://mathiasbynens.github.io/rel-noopener/
|
||||
if (newWindow) {
|
||||
newWindow.opener = null;
|
||||
newWindow.location = normalizeLink(url);
|
||||
let customEvent;
|
||||
if (this.props.onLinkOpen) {
|
||||
customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent);
|
||||
this.props.onLinkOpen(this.hitLinkElement, customEvent);
|
||||
}
|
||||
if (!customEvent?.defaultPrevented) {
|
||||
const target = isLocalLink(url) ? "_self" : "_blank";
|
||||
const newWindow = window.open(undefined, target);
|
||||
// https://mathiasbynens.github.io/rel-noopener/
|
||||
if (newWindow) {
|
||||
newWindow.opener = null;
|
||||
newWindow.location = normalizeLink(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2896,7 +2906,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.hitLinkElement &&
|
||||
!this.state.selectedElementIds[this.hitLinkElement.id]
|
||||
) {
|
||||
this.redirectToLink();
|
||||
this.redirectToLink(event);
|
||||
}
|
||||
|
||||
this.removePointer(event);
|
||||
|
@ -52,6 +52,8 @@ export enum EVENT {
|
||||
HASHCHANGE = "hashchange",
|
||||
VISIBILITY_CHANGE = "visibilitychange",
|
||||
SCROLL = "scroll",
|
||||
// custom events
|
||||
EXCALIDRAW_LINK = "excalidraw-link",
|
||||
}
|
||||
|
||||
export const ENV = {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { AppState, Point } from "../types";
|
||||
import { AppState, ExcalidrawProps, Point } from "../types";
|
||||
import {
|
||||
getShortcutKey,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
wrapEvent,
|
||||
} from "../utils";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
@ -48,10 +49,12 @@ export const Hyperlink = ({
|
||||
element,
|
||||
appState,
|
||||
setAppState,
|
||||
onLinkOpen,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
appState: AppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||
}) => {
|
||||
const linkVal = element.link || "";
|
||||
|
||||
@ -159,6 +162,18 @@ export const Hyperlink = ({
|
||||
"d-none": isEditing,
|
||||
})}
|
||||
target={isLocalLink(element.link) ? "_self" : "_blank"}
|
||||
onClick={(event) => {
|
||||
if (element.link && onLinkOpen) {
|
||||
const customEvent = wrapEvent(
|
||||
EVENT.EXCALIDRAW_LINK,
|
||||
event.nativeEvent,
|
||||
);
|
||||
onLinkOpen(element, customEvent);
|
||||
if (customEvent.defaultPrevented) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{element.link}
|
||||
|
@ -19,6 +19,7 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `onLinkOpen` prop which will be triggered when clicked on element hyperlink if present [#4694](https://github.com/excalidraw/excalidraw/pull/4694).
|
||||
- Support updating library using [`updateScene`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene) API [#4546](https://github.com/excalidraw/excalidraw/pull/4546).
|
||||
|
||||
- Introduced primary colors to the app. The colors can be overriden. Check [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#customizing-styles) on how to do so.
|
||||
|
@ -405,6 +405,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
|
||||
| [`onLibraryChange`](#onLibraryChange) | <pre>(items: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => void | Promise<any> </pre> | | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||
| [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load |
|
||||
| [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise<string>` | Allows you to override `id` generation for files added on canvas |
|
||||
| [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked |
|
||||
|
||||
### Dimensions of Excalidraw
|
||||
|
||||
@ -704,6 +705,39 @@ Allows you to override `id` generation for files added on canvas (images). By de
|
||||
(file: File) => string | Promise<string>
|
||||
```
|
||||
|
||||
#### `onLinkOpen`
|
||||
|
||||
This prop if passed will be triggered when clicked on link. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
|
||||
|
||||
```
|
||||
(element: ExcalidrawElement, event: CustomEvent<{ nativeEvent: MouseEvent }>) => void
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
const history = useHistory();
|
||||
|
||||
// open internal links using the app's router, but opens external links in
|
||||
// a new tab/window
|
||||
const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
|
||||
(element, event) => {
|
||||
const link = element.link;
|
||||
const { nativeEvent } = event.detail;
|
||||
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
|
||||
const isNewWindow = nativeEvent.shiftKey;
|
||||
const isInternalLink =
|
||||
link.startsWith("/") || link.includes(window.location.origin);
|
||||
if (isInternalLink && !isNewTab && !isNewWindow) {
|
||||
history.push(link.replace(window.location.origin, ""));
|
||||
// signal that we're handling the redirect ourselves
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
[history],
|
||||
);
|
||||
```
|
||||
|
||||
### Does it support collaboration ?
|
||||
|
||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
import InitialData from "./initialData";
|
||||
import Sidebar from "./sidebar/Sidebar";
|
||||
@ -87,6 +87,21 @@ export default function App() {
|
||||
excalidrawRef.current.updateScene(sceneData);
|
||||
};
|
||||
|
||||
const onLinkOpen = useCallback((element, event) => {
|
||||
const link = element.link;
|
||||
const { nativeEvent } = event.detail;
|
||||
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
|
||||
const isNewWindow = nativeEvent.shiftKey;
|
||||
const isInternalLink =
|
||||
link.startsWith("/") || link.includes(window.location.origin);
|
||||
if (isInternalLink && !isNewTab && !isNewWindow) {
|
||||
// signal that we're handling the redirect ourselves
|
||||
event.preventDefault();
|
||||
// do a custom redirect, such as passing to react-router
|
||||
// ...
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1> Excalidraw Example</h1>
|
||||
@ -179,6 +194,7 @@ export default function App() {
|
||||
UIOptions={{ canvasActions: { loadScene: false } }}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderFooter={renderFooter}
|
||||
onLinkOpen={onLinkOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -35,6 +35,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
onLibraryChange,
|
||||
autoFocus = false,
|
||||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
@ -96,6 +97,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={autoFocus}
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
/>
|
||||
</InitializeApp>
|
||||
);
|
||||
|
@ -247,6 +247,12 @@ export interface ExcalidrawProps {
|
||||
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
||||
autoFocus?: boolean;
|
||||
generateIdForFile?: (file: File) => string | Promise<string>;
|
||||
onLinkOpen?: (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
event: CustomEvent<{
|
||||
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
|
||||
}>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
|
10
src/utils.ts
10
src/utils.ts
@ -2,6 +2,7 @@ import colors from "./colors";
|
||||
import {
|
||||
CURSOR_TYPE,
|
||||
DEFAULT_VERSION,
|
||||
EVENT,
|
||||
FONT_FAMILY,
|
||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||
} from "./constants";
|
||||
@ -523,3 +524,12 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||
|
||||
export const isTestEnv = () =>
|
||||
typeof process !== "undefined" && process.env?.NODE_ENV === "test";
|
||||
|
||||
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
|
||||
return new CustomEvent(name, {
|
||||
detail: {
|
||||
nativeEvent,
|
||||
},
|
||||
cancelable: true,
|
||||
});
|
||||
};
|
||||
|
38
yarn.lock
38
yarn.lock
@ -2540,18 +2540,18 @@
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.19.0.tgz"
|
||||
integrity sha512-A4iAlexVvd4IBsSTNxdvdepW0D4uR/fwxDrKUa+iEY9UWvGREu2ZyB8ylTENM1SH8F7bVC9ac9+si3LWNxcBuA==
|
||||
|
||||
"@typescript-eslint/types@5.3.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.0.tgz#af29fd53867c2df0028c57c36a655bd7e9e05416"
|
||||
integrity sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg==
|
||||
"@typescript-eslint/types@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.2.tgz#604d15d795c4601fffba6ecb4587ff9fdec68ce8"
|
||||
integrity sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@3.10.1", "@typescript-eslint/typescript-estree@4.19.0", "@typescript-eslint/typescript-estree@5.3.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz#4f68ddd46dc2983182402d2ab21fb44ad94988cf"
|
||||
integrity sha512-FJ0nqcaUOpn/6Z4Jwbtf+o0valjBLkqc3MWkMvrhA2TvzFXtcclIM8F4MBEmYa2kgcI8EZeSAzwoSrIC8JYkug==
|
||||
"@typescript-eslint/typescript-estree@3.10.1", "@typescript-eslint/typescript-estree@4.19.0", "@typescript-eslint/typescript-estree@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz#810906056cd3ddcb35aa333fdbbef3713b0fe4a7"
|
||||
integrity sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.3.0"
|
||||
"@typescript-eslint/visitor-keys" "5.3.0"
|
||||
"@typescript-eslint/types" "5.10.2"
|
||||
"@typescript-eslint/visitor-keys" "5.10.2"
|
||||
debug "^4.3.2"
|
||||
globby "^11.0.4"
|
||||
is-glob "^4.0.3"
|
||||
@ -2566,12 +2566,12 @@
|
||||
"@typescript-eslint/types" "4.19.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.3.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.0.tgz#a6258790f3b7b2547f70ed8d4a1e0c3499994523"
|
||||
integrity sha512-oVIAfIQuq0x2TFDNLVavUn548WL+7hdhxYn+9j3YdJJXB7mH9dAmZNJsPDa7Jc+B9WGqoiex7GUDbyMxV0a/aw==
|
||||
"@typescript-eslint/visitor-keys@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz#fdbf272d8e61c045d865bd6c8b41bea73d222f3d"
|
||||
integrity sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.3.0"
|
||||
"@typescript-eslint/types" "5.10.2"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@webassemblyjs/ast@1.9.0":
|
||||
@ -14392,10 +14392,10 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
|
||||
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
|
||||
typescript@4.5.5:
|
||||
version "4.5.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
|
||||
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
|
||||
|
||||
typeson-registry@^1.0.0-alpha.20:
|
||||
version "1.0.0-alpha.39"
|
||||
|
Loading…
x
Reference in New Issue
Block a user