feat: support multi-embed pasting & x.com domain (#7516)
This commit is contained in:
parent
4249b7dec8
commit
43ccc875fb
@ -182,6 +182,7 @@ import {
|
|||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
IframeData,
|
IframeData,
|
||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
@ -271,11 +272,12 @@ import {
|
|||||||
easeOut,
|
easeOut,
|
||||||
updateStable,
|
updateStable,
|
||||||
addEventListener,
|
addEventListener,
|
||||||
|
normalizeEOL,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
createSrcDoc,
|
createSrcDoc,
|
||||||
embeddableURLValidator,
|
embeddableURLValidator,
|
||||||
extractSrc,
|
maybeParseEmbedSrc,
|
||||||
getEmbedLink,
|
getEmbedLink,
|
||||||
} from "../element/embeddable";
|
} from "../element/embeddable";
|
||||||
import {
|
import {
|
||||||
@ -2924,21 +2926,49 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
const maybeUrl = extractSrc(data.text);
|
const nonEmptyLines = normalizeEOL(data.text)
|
||||||
|
.split(/\n+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const embbeddableUrls = nonEmptyLines
|
||||||
|
.map((str) => maybeParseEmbedSrc(str))
|
||||||
|
.filter((string) => {
|
||||||
|
return (
|
||||||
|
embeddableURLValidator(string, this.props.validateEmbeddable) &&
|
||||||
|
(/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(string) ||
|
||||||
|
getEmbedLink(string)?.type === "video")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isPlainPaste &&
|
!IS_PLAIN_PASTE &&
|
||||||
embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) &&
|
embbeddableUrls.length > 0 &&
|
||||||
(/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(maybeUrl) ||
|
// if there were non-embeddable text (lines) mixed in with embeddable
|
||||||
getEmbedLink(maybeUrl)?.type === "video")
|
// urls, ignore and paste as text
|
||||||
|
embbeddableUrls.length === nonEmptyLines.length
|
||||||
) {
|
) {
|
||||||
|
const embeddables: NonDeleted<ExcalidrawEmbeddableElement>[] = [];
|
||||||
|
for (const url of embbeddableUrls) {
|
||||||
|
const prevEmbeddable: ExcalidrawEmbeddableElement | undefined =
|
||||||
|
embeddables[embeddables.length - 1];
|
||||||
const embeddable = this.insertEmbeddableElement({
|
const embeddable = this.insertEmbeddableElement({
|
||||||
sceneX,
|
sceneX: prevEmbeddable
|
||||||
|
? prevEmbeddable.x + prevEmbeddable.width + 20
|
||||||
|
: sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
link: normalizeLink(maybeUrl),
|
link: normalizeLink(url),
|
||||||
});
|
});
|
||||||
if (embeddable) {
|
if (embeddable) {
|
||||||
this.setState({ selectedElementIds: { [embeddable.id]: true } });
|
embeddables.push(embeddable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (embeddables.length) {
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: Object.fromEntries(
|
||||||
|
embeddables.map((embeddable) => [embeddable.id, true]),
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -32,9 +32,9 @@ const RE_GH_GIST_EMBED =
|
|||||||
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
|
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
|
||||||
|
|
||||||
// not anchored to start to allow <blockquote> twitter embeds
|
// not anchored to start to allow <blockquote> twitter embeds
|
||||||
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:twitter|x).com/;
|
||||||
const RE_TWITTER_EMBED =
|
const RE_TWITTER_EMBED =
|
||||||
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
|
/^<blockquote[\s\S]*?\shref=["'](https:\/\/(?:twitter|x).com\/[^"']*)/i;
|
||||||
|
|
||||||
const RE_VALTOWN =
|
const RE_VALTOWN =
|
||||||
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
||||||
@ -54,6 +54,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"link.excalidraw.com",
|
"link.excalidraw.com",
|
||||||
"gist.github.com",
|
"gist.github.com",
|
||||||
"twitter.com",
|
"twitter.com",
|
||||||
|
"x.com",
|
||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
@ -155,6 +156,9 @@ export const getEmbedLink = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (RE_TWITTER.test(link)) {
|
if (RE_TWITTER.test(link)) {
|
||||||
|
// the embed srcdoc still supports twitter.com domain only
|
||||||
|
link = link.replace(/\bx.com\b/, "twitter.com");
|
||||||
|
|
||||||
let ret: IframeData;
|
let ret: IframeData;
|
||||||
// assume embed code
|
// assume embed code
|
||||||
if (/<blockquote/.test(link)) {
|
if (/<blockquote/.test(link)) {
|
||||||
@ -321,26 +325,26 @@ const validateHostname = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractSrc = (htmlString: string): string => {
|
export const maybeParseEmbedSrc = (str: string): string => {
|
||||||
const twitterMatch = htmlString.match(RE_TWITTER_EMBED);
|
const twitterMatch = str.match(RE_TWITTER_EMBED);
|
||||||
if (twitterMatch && twitterMatch.length === 2) {
|
if (twitterMatch && twitterMatch.length === 2) {
|
||||||
return twitterMatch[1];
|
return twitterMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
|
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
||||||
if (gistMatch && gistMatch.length === 2) {
|
if (gistMatch && gistMatch.length === 2) {
|
||||||
return gistMatch[1];
|
return gistMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RE_GIPHY.test(htmlString)) {
|
if (RE_GIPHY.test(str)) {
|
||||||
return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
|
return `https://giphy.com/embed/${RE_GIPHY.exec(str)![1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = htmlString.match(RE_GENERIC_EMBED);
|
const match = str.match(RE_GENERIC_EMBED);
|
||||||
if (match && match.length === 2) {
|
if (match && match.length === 2) {
|
||||||
return match[1];
|
return match[1];
|
||||||
}
|
}
|
||||||
return htmlString;
|
return str;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const embeddableURLValidator = (
|
export const embeddableURLValidator = (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
@ -39,15 +39,13 @@ import { ExtractSetType } from "../utility-types";
|
|||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
text
|
normalizeEOL(text)
|
||||||
// replace tabs with spaces so they render and measure correctly
|
// replace tabs with spaces so they render and measure correctly
|
||||||
.replace(/\t/g, " ")
|
.replace(/\t/g, " ")
|
||||||
// normalize newlines
|
|
||||||
.replace(/\r?\n|\r/g, "\n")
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const splitIntoLines = (text: string) => {
|
const splitIntoLines = (text: string) => {
|
||||||
return normalizeText(text).split("\n");
|
return normalizeText(text).split("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1071,3 +1071,7 @@ export function addEventListener(
|
|||||||
target?.removeEventListener?.(type, listener, options);
|
target?.removeEventListener?.(type, listener, options);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeEOL = (str: string) => {
|
||||||
|
return str.replace(/\r?\n|\r/g, "\n");
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user