mirror of
https://github.com/outline/outline.git
synced 2026-04-25 11:48:57 -05:00
72cc740b1c
* Add clipboard permissions to embedded Frame component - Add clipboard-read and clipboard-write permissions to iframe allow policy - Ensure clipboard permissions are always included even when custom allow prop is provided - Update Frame component to properly handle allow prop parameter * Simplify clipboard permissions implementation - Remove allow prop handling from Frame component - Simply add clipboard-read and clipboard-write to default permissions list - Keep implementation minimal as requested --------- Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
165 lines
3.9 KiB
TypeScript
165 lines
3.9 KiB
TypeScript
import { OpenIcon } from "outline-icons";
|
|
import * as React from "react";
|
|
import { useState, useEffect, useRef } from "react";
|
|
import styled from "styled-components";
|
|
import { Optional } from "utility-types";
|
|
import { s } from "../../styles";
|
|
import { sanitizeUrl } from "../../utils/urls";
|
|
|
|
type Props = Omit<
|
|
Optional<React.ComponentProps<typeof Iframe>>,
|
|
"children" | "style"
|
|
> & {
|
|
/** The URL to load in the iframe */
|
|
src?: string;
|
|
/** Whether to display a border, defaults to true */
|
|
border?: boolean;
|
|
/** The aria title of the frame */
|
|
title?: string;
|
|
/** An icon to display under the frame representing the service */
|
|
icon?: React.ReactNode;
|
|
/** The canonical URL of the content */
|
|
canonicalUrl?: string;
|
|
/** Whether the node is currently selected */
|
|
isSelected?: boolean;
|
|
/** Additional styling */
|
|
style?: React.CSSProperties;
|
|
/** The allow policy of the frame */
|
|
allow?: string;
|
|
};
|
|
|
|
type PropsWithRef = Props & {
|
|
forwardedRef: React.Ref<HTMLIFrameElement>;
|
|
};
|
|
|
|
const Frame = ({
|
|
border,
|
|
style = {},
|
|
forwardedRef,
|
|
icon,
|
|
title,
|
|
canonicalUrl,
|
|
isSelected,
|
|
referrerPolicy,
|
|
className = "",
|
|
src,
|
|
...rest
|
|
}: PropsWithRef) => {
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const mountedRef = useRef(true);
|
|
|
|
useEffect(() => {
|
|
// Set mounted flag
|
|
mountedRef.current = true;
|
|
|
|
// Load iframe after a small delay
|
|
const timer = setTimeout(() => {
|
|
if (mountedRef.current) {
|
|
setIsLoaded(true);
|
|
}
|
|
}, 0);
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
mountedRef.current = false;
|
|
clearTimeout(timer);
|
|
};
|
|
}, []);
|
|
|
|
const showBottomBar = !!(icon || canonicalUrl);
|
|
|
|
return (
|
|
<Rounded
|
|
style={style}
|
|
$showBottomBar={showBottomBar}
|
|
$border={border}
|
|
className={
|
|
isSelected ? `ProseMirror-selectednode ${className}` : className
|
|
}
|
|
>
|
|
{isLoaded && (
|
|
<Iframe
|
|
ref={forwardedRef}
|
|
$showBottomBar={showBottomBar}
|
|
sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-downloads allow-storage-access-by-user-activation"
|
|
allow="fullscreen; encrypted-media; picture-in-picture; clipboard-read; clipboard-write"
|
|
style={style}
|
|
frameBorder="0"
|
|
title="embed"
|
|
loading="lazy"
|
|
src={sanitizeUrl(src)}
|
|
referrerPolicy={referrerPolicy}
|
|
allowFullScreen
|
|
{...rest}
|
|
/>
|
|
)}
|
|
{showBottomBar && (
|
|
<Bar>
|
|
{icon} <Title>{title}</Title>
|
|
{canonicalUrl && (
|
|
<Open href={canonicalUrl} target="_blank" rel="noopener noreferrer">
|
|
<OpenIcon size={18} /> Open
|
|
</Open>
|
|
)}
|
|
</Bar>
|
|
)}
|
|
</Rounded>
|
|
);
|
|
};
|
|
|
|
const Iframe = styled.iframe<{ $showBottomBar: boolean }>`
|
|
border-radius: ${(props) => (props.$showBottomBar ? "3px 3px 0 0" : "3px")};
|
|
display: block;
|
|
`;
|
|
|
|
const Rounded = styled.div<{
|
|
$showBottomBar: boolean;
|
|
$border?: boolean;
|
|
}>`
|
|
border: 1px solid
|
|
${(props) => (props.$border ? props.theme.embedBorder : "transparent")};
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
|
|
${(props) =>
|
|
props.$showBottomBar &&
|
|
`
|
|
padding-bottom: 28px;
|
|
`}
|
|
`;
|
|
|
|
const Open = styled.a`
|
|
color: ${s("textSecondary")} !important;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
align-items: center;
|
|
display: flex;
|
|
position: absolute;
|
|
right: 0;
|
|
padding: 0 8px;
|
|
`;
|
|
|
|
const Title = styled.span`
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
padding-left: 4px;
|
|
`;
|
|
|
|
const Bar = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
border-top: 1px solid ${(props) => props.theme.embedBorder};
|
|
background: ${s("backgroundSecondary")};
|
|
color: ${s("textSecondary")};
|
|
padding: 0 8px;
|
|
border-bottom-left-radius: 6px;
|
|
border-bottom-right-radius: 6px;
|
|
user-select: none;
|
|
height: 28px;
|
|
position: relative;
|
|
`;
|
|
|
|
export default React.forwardRef<HTMLIFrameElement, Props>((props, ref) => (
|
|
<Frame {...props} forwardedRef={ref} />
|
|
));
|