139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
|
|
import { useCallback, useState } from "react";
|
|
|
|
export type IEmbedShape = TLBaseShape<
|
|
'Embed',
|
|
{
|
|
w: number;
|
|
h: number;
|
|
url: string | null;
|
|
}
|
|
>;
|
|
|
|
export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
|
static override type = 'Embed';
|
|
|
|
getDefaultProps(): IEmbedShape['props'] {
|
|
return {
|
|
url: null,
|
|
w: 640,
|
|
h: 480,
|
|
};
|
|
}
|
|
|
|
indicator(shape: IEmbedShape) {
|
|
return (
|
|
<g>
|
|
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
component(shape: IEmbedShape) {
|
|
const [inputUrl, setInputUrl] = useState(shape.props.url || '');
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = useCallback((e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`;
|
|
|
|
// Handle YouTube links
|
|
if (completedUrl.includes('youtube.com') || completedUrl.includes('youtu.be')) {
|
|
const videoId = extractYouTubeVideoId(completedUrl);
|
|
if (videoId) {
|
|
completedUrl = `https://www.youtube.com/embed/${videoId}`;
|
|
} else {
|
|
setError('Invalid YouTube URL');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle Google Docs links
|
|
if (completedUrl.includes('docs.google.com')) {
|
|
const docId = completedUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
|
|
if (docId) {
|
|
completedUrl = `https://docs.google.com/document/d/${docId}/preview`;
|
|
} else {
|
|
setError('Invalid Google Docs URL');
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.editor.updateShape<IEmbedShape>({ id: shape.id, type: 'Embed', props: { ...shape.props, url: completedUrl } });
|
|
|
|
// Check if the URL is valid
|
|
const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//);
|
|
if (!isValidUrl) {
|
|
setError('Invalid website URL');
|
|
} else {
|
|
setError('');
|
|
}
|
|
}, [inputUrl]);
|
|
|
|
const extractYouTubeVideoId = (url: string): string | null => {
|
|
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
|
|
const match = url.match(regExp);
|
|
return (match && match[2].length === 11) ? match[2] : null;
|
|
};
|
|
|
|
const wrapperStyle = {
|
|
width: `${shape.props.w}px`,
|
|
height: `${shape.props.h}px`,
|
|
padding: '15px',
|
|
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
backgroundColor: '#F0F0F0',
|
|
borderRadius: '4px',
|
|
};
|
|
|
|
const contentStyle = {
|
|
pointerEvents: 'all' as const,
|
|
width: '100%',
|
|
height: '100%',
|
|
border: '1px solid #D3D3D3',
|
|
backgroundColor: '#FFFFFF',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
overflow: 'hidden',
|
|
};
|
|
|
|
if (!shape.props.url) {
|
|
return (
|
|
<div style={wrapperStyle}>
|
|
<div style={contentStyle} onClick={() => document.querySelector('input')?.focus()}>
|
|
<form onSubmit={handleSubmit} style={{ width: '100%', height: '100%', padding: '10px' }}>
|
|
<input
|
|
type="text"
|
|
value={inputUrl}
|
|
onChange={(e) => setInputUrl(e.target.value)}
|
|
placeholder="Enter URL"
|
|
style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
handleSubmit(e);
|
|
}
|
|
}}
|
|
/>
|
|
{error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={wrapperStyle}>
|
|
<div style={contentStyle}>
|
|
<iframe
|
|
src={shape.props.url}
|
|
width="100%"
|
|
height="100%"
|
|
style={{ border: 'none' }}
|
|
allowFullScreen
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|