embed-basic/hooks.tsx (227 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useState, ReactElement, isValidElement } from 'react';
import { createRoot } from 'react-dom/client';
import {
GithubGistEmbed,
CodePenEmbed,
YouTubeEmbed,
JSFiddleEmbed,
FigmaEmbed,
ExcalidrawEmbed,
LoomEmbed,
DropboxEmbed,
TwitterEmbed,
} from './components';
import { Request } from './types';
interface Config {
platform: string;
enable: boolean;
}
const get = async (url: string) => {
const response = await fetch(url);
const { data } = await response.json();
return data;
};
const useRenderEmbed = (
element,
request: Request = {
get,
},
) => {
const [configs, setConfigs] = useState<Config[] | null>(null);
const embeds = [
{
name: 'YouTube',
regexs: [
/https:\/\/youtu\.be\/([a-zA-Z0-9_-]{11})/,
/https:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/,
/https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
],
embed: (videoId: string) => {
return <YouTubeEmbed videoId={videoId} />;
},
},
{
name: 'Twitter',
regexs: [
/https:\/\/twitter\.com\/[a-zA-Z0-9_]+\/status\/([a-zA-Z0-9_]+)/,
/https:\/\/x\.com\/[a-zA-Z0-9_]+\/status\/([a-zA-Z0-9_]+)/,
],
embed: (_, url, title = '') => {
const blockquoteElement = document.createElement('blockquote');
blockquoteElement.classList.add('twitter-tweet');
const anchorElement = document.createElement('a');
anchorElement.href = url.replace('x.com', 'twitter.com');
anchorElement.textContent = title;
blockquoteElement.appendChild(anchorElement);
const scriptElement = document.createElement('script');
scriptElement.src = 'https://platform.twitter.com/widgets.js';
scriptElement.async = true;
const styleElement = document.createElement('style');
styleElement.innerHTML = `
.twitter-tweet {
display: block;
margin: 0 auto;
}
`;
return (
<TwitterEmbed
url={url.replace('x.com', 'twitter.com')}
title={title}
/>
);
},
},
{
name: 'CodePen',
regexs: [
/https:\/\/codepen\.io\/[a-zA-Z0-9_]+\/pen\/([a-zA-Z0-9_]+)/,
/https:\/\/codepen\.io\/[a-zA-Z0-9_]+\/full\/([a-zA-Z0-9_]+)/,
],
embed: (penId) => {
return <CodePenEmbed penId={penId} />;
},
},
{
name: 'JSFiddle',
regexs: [
/https:\/\/jsfiddle\.net\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/,
/https:\/\/jsfiddle\.net\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)\/embed/,
],
embed: (fiddleId: string) => {
return <JSFiddleEmbed fiddleId={fiddleId} />;
},
},
{
name: 'GithubGist',
regexs: [
/https:\/\/gist\.github\.com\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/,
/https:\/\/gist\.github\.com\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)\.js/,
],
embed: (_, url) => {
const scriptUrl = url.indexOf('.js') > -1 ? url : `${url}.js`;
return <GithubGistEmbed scriptUrl={scriptUrl} />;
},
},
{
name: 'Figma',
regexs: [
/https:\/\/www\.figma\.com\/design\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/,
/https:\/\/www\.figma\.com\/file\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/,
],
embed: (_, url) => {
return <FigmaEmbed url={url} />;
},
},
{
name: 'Excalidraw',
regexs: [
/https:\/\/excalidraw\.com\/#json=([a-zA-Z0-9_,-]+)/,
/https:\/\/excalidraw\.com\/([a-zA-Z0-9_,-]+)/,
],
embed: (excalidrawId: string) => {
return <ExcalidrawEmbed excalidrawId={excalidrawId} />;
},
},
{
name: 'Loom',
regexs: [
/https:\/\/www\.loom\.com\/embed\/([a-zA-Z0-9_]+)/,
/https:\/\/www\.loom\.com\/share\/([a-zA-Z0-9_]+)/,
],
embed: (loomId: string) => {
return <LoomEmbed loomId={loomId} />;
},
},
{
name: 'Dropbox',
regexs: [
/https:\/\/www\.dropbox\.com\/s\/([a-zA-Z0-9_]+)\/[a-zA-Z0-9_]+/,
],
embed: (dropboxId: string) => {
return <DropboxEmbed dropboxId={dropboxId} />;
},
},
];
const filteredEmbeds = embeds.filter((embed) => {
const finded = configs?.find(
(config) => config.platform === embed.name && config.enable,
);
return finded;
});
const renderEmbed = (
url: string,
title: string,
): string | HTMLElement | HTMLElement[] => {
let html: string | HTMLElement | HTMLElement[] | ReactElement = '';
filteredEmbeds.forEach((embed) => {
if (html) return;
embed.regexs.forEach((regex) => {
if (html) return;
const match = url.match(regex);
if (match) {
html = embed.embed(match[1], url, title);
}
});
});
return html;
};
const render = (targetElement) => {
if (!element) {
return;
}
const links = targetElement.querySelectorAll('a');
let hasDefaultStyle = false;
links.forEach((link) => {
const url = link.getAttribute('href') || '';
const title = link.getAttribute('title') || '';
if (!url) {
return;
}
if (title !== '@embed') {
return;
}
const embed = renderEmbed(url, link?.textContent || '');
if (isValidElement(embed)) {
const parentElement = link.parentElement as HTMLElement;
parentElement.classList.add('position-relative');
parentElement.style.height = '128px';
createRoot(parentElement).render(embed);
} else {
hasDefaultStyle = true;
link.innerHTML = `
<div class="card embed-light">
<div class="card-body">
<div class="text-secondary small mb-1">${url}</div>
<div class="text-body fw-bold">${link.textContent}</div>
</div>
</div>
`;
}
});
// default card style add embed-ligh class for hover bg-light
let styleElement = document.querySelector('style#embed-style');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'embed-style';
if (hasDefaultStyle) {
styleElement.textContent = `
.embed-light:hover {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;
}
`;
const head = document.querySelector('head') as HTMLElement;
head.appendChild(styleElement);
}
}
};
const getConfig = () => {
request
.get('/answer/api/v1/embed/config')
.then((result) => setConfigs(result));
};
useEffect(() => {
getConfig();
return () => {
const styleEle = document.querySelector('style#embed-style');
const head = document.querySelector('head') as HTMLElement;
if (styleEle) {
head.removeChild(styleEle);
}
};
}, []);
useEffect(() => {
if (!element) {
return;
}
if (!configs) {
return;
}
let targetElement;
if (element instanceof HTMLElement) {
targetElement = element;
} else {
targetElement = element.current;
}
render(targetElement);
const observer = new MutationObserver(() => {
render(targetElement);
});
observer.observe(targetElement, {
childList: true,
});
return () => {
observer.disconnect();
};
}, [element, configs]);
};
export { useRenderEmbed };