Skip to content

Commit

Permalink
feat: Allow resizing of embed height (#8154)
Browse files Browse the repository at this point in the history
* stash

* tsc

* remove console log

* Restore bottom bar on embeds

* fix: Cannot see selected state

* fix layout issue
  • Loading branch information
tommoor authored Jan 2, 2025
1 parent 4789ddd commit d645915
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 105 deletions.
191 changes: 136 additions & 55 deletions shared/editor/components/Embed.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,154 @@
import * as React from "react";
import styled from "styled-components";
import { EmbedDescriptor } from "../embeds";
import { getMatchingEmbed } from "../lib/embeds";
import { ComponentProps } from "../types";
import DisabledEmbed from "./DisabledEmbed";
import Frame from "./Frame";
import { ResizeBottom, ResizeLeft, ResizeRight } from "./ResizeHandle";
import useDragResize from "./hooks/useDragResize";

const EmbedComponent = ({
isEditable,
isSelected,
theme,
node,
embeds,
embedsDisabled,
}: ComponentProps & {
type Props = ComponentProps & {
embeds: EmbedDescriptor[];
embedsDisabled?: boolean;
}) => {
const cache = React.useMemo(
() => getMatchingEmbed(embeds, node.attrs.href),
[embeds, node.attrs.href]
style?: React.CSSProperties;
onChangeSize?: (props: { width: number; height?: number }) => void;
};

const Embed = (props: Props) => {
const ref = React.useRef<HTMLIFrameElement>(null);
const { node, isEditable, onChangeSize } = props;
const naturalWidth = 10000;
const naturalHeight = 400;
const isResizable = !!onChangeSize;

const { width, height, setSize, handlePointerDown, dragging } = useDragResize(
{
width: node.attrs.width ?? naturalWidth,
height: node.attrs.height ?? naturalHeight,
naturalWidth,
naturalHeight,
gridSnap: 5,
onChangeSize,
ref,
}
);

if (!cache) {
return null;
}
React.useEffect(() => {
if (node.attrs.height && node.attrs.height !== height) {
setSize({
width: node.attrs.width,
height: node.attrs.height,
});
}
}, [node.attrs.height]);

const { embed, matches } = cache;

if (embedsDisabled) {
return (
<DisabledEmbed
href={node.attrs.href}
embed={embed}
isEditable={isEditable}
isSelected={isSelected}
theme={theme}
/>
);
}
const style: React.CSSProperties = {
width: width || "100%",
height: height || 400,
maxWidth: "100%",
pointerEvents: dragging ? "none" : "all",
};

if (embed.transformMatch) {
const src = embed.transformMatch(matches);
return (
<Frame
src={src}
isSelected={isSelected}
canonicalUrl={embed.hideToolbar ? undefined : node.attrs.href}
title={embed.title}
referrerPolicy="origin"
border
/>
);
}
return (
<FrameWrapper ref={ref}>
<InnerEmbed ref={ref} style={style} {...props} />
{isEditable && isResizable && (
<>
<ResizeBottom
onPointerDown={handlePointerDown("bottom")}
$dragging={!!dragging}
/>
</>
)}
</FrameWrapper>
);
};

if ("component" in embed) {
return (
// @ts-expect-error Component type
<embed.component
attrs={node.attrs}
matches={matches}
isEditable={isEditable}
isSelected={isSelected}
embed={embed}
theme={theme}
/>
const InnerEmbed = React.forwardRef<HTMLIFrameElement, Props>(
function InnerEmbed_(
{ isEditable, isSelected, theme, node, embeds, embedsDisabled, style },
ref
) {
const cache = React.useMemo(
() => getMatchingEmbed(embeds, node.attrs.href),
[embeds, node.attrs.href]
);

if (!cache) {
return null;
}

const { embed, matches } = cache;

if (embedsDisabled) {
return (
<DisabledEmbed
href={node.attrs.href}
embed={embed}
isEditable={isEditable}
isSelected={isSelected}
theme={theme}
/>
);
}

if (embed.transformMatch) {
const src = embed.transformMatch(matches);
return (
<Frame
ref={ref}
src={src}
style={style}
isSelected={isSelected}
canonicalUrl={embed.hideToolbar ? undefined : node.attrs.href}
title={embed.title}
referrerPolicy="origin"
border
/>
);
}

if ("component" in embed) {
return (
// @ts-expect-error Component type
<embed.component
ref={ref}
attrs={node.attrs}
style={style}
matches={matches}
isEditable={isEditable}
isSelected={isSelected}
embed={embed}
theme={theme}
/>
);
}

return null;
}
);

return null;
};
const FrameWrapper = styled.div`
line-height: 0;
position: relative;
margin-left: auto;
margin-right: auto;
white-space: nowrap;
cursor: default;
border-radius: 8px;
user-select: none;
max-width: 100%;
transition-property: width, max-height;
transition-duration: 150ms;
transition-timing-function: ease-in-out;
&:hover {
${ResizeLeft}, ${ResizeRight} {
opacity: 1;
}
}
`;

export default EmbedComponent;
export default Embed;
42 changes: 20 additions & 22 deletions shared/editor/components/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Optional } from "utility-types";
import { s } from "../../styles";
import { sanitizeUrl } from "../../utils/urls";

type Props = Omit<Optional<HTMLIFrameElement>, "children"> & {
type Props = Omit<Optional<HTMLIFrameElement>, "children" | "style"> & {
/** The URL to load in the iframe */
src?: string;
/** Whether to display a border, defaults to true */
Expand All @@ -20,10 +20,8 @@ type Props = Omit<Optional<HTMLIFrameElement>, "children"> & {
canonicalUrl?: string;
/** Whether the node is currently selected */
isSelected?: boolean;
/** The width of the frame */
width?: string;
/** The height of the frame */
height?: string;
/** Additional styling */
style?: React.CSSProperties;
/** The allow policy of the frame */
allow?: string;
};
Expand Down Expand Up @@ -58,8 +56,7 @@ class Frame extends React.Component<PropsWithRef> {
render() {
const {
border,
width = "100%",
height = "400px",
style = {},
forwardedRef,
icon,
title,
Expand All @@ -69,13 +66,12 @@ class Frame extends React.Component<PropsWithRef> {
className = "",
src,
} = this.props;
const withBar = !!(icon || canonicalUrl);
const showBottomBar = !!(icon || canonicalUrl);

return (
<Rounded
width={width}
height={height}
$withBar={withBar}
style={style}
$showBottomBar={showBottomBar}
$border={border}
className={
isSelected ? `ProseMirror-selectednode ${className}` : className
Expand All @@ -84,10 +80,9 @@ class Frame extends React.Component<PropsWithRef> {
{this.isLoaded && (
<Iframe
ref={forwardedRef}
$withBar={withBar}
$showBottomBar={showBottomBar}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-downloads allow-storage-access-by-user-activation"
width={width}
height={height}
style={style}
frameBorder="0"
title="embed"
loading="lazy"
Expand All @@ -96,7 +91,7 @@ class Frame extends React.Component<PropsWithRef> {
allowFullScreen
/>
)}
{withBar && (
{showBottomBar && (
<Bar>
{icon} <Title>{title}</Title>
{canonicalUrl && (
Expand All @@ -115,23 +110,25 @@ class Frame extends React.Component<PropsWithRef> {
}
}

const Iframe = styled.iframe<{ $withBar: boolean }>`
border-radius: ${(props) => (props.$withBar ? "3px 3px 0 0" : "3px")};
const Iframe = styled.iframe<{ $showBottomBar: boolean }>`
border-radius: ${(props) => (props.$showBottomBar ? "3px 3px 0 0" : "3px")};
display: block;
`;

const Rounded = styled.div<{
width: string;
height: string;
$withBar: boolean;
$showBottomBar: boolean;
$border?: boolean;
}>`
border: 1px solid
${(props) => (props.$border ? props.theme.embedBorder : "transparent")};
border-radius: 6px;
overflow: hidden;
width: ${(props) => props.width};
height: ${(props) => (props.$withBar ? props.height + 28 : props.height)};
${(props) =>
props.$showBottomBar &&
`
padding-bottom: 28px;
`}
`;

const Open = styled.a`
Expand Down Expand Up @@ -161,6 +158,7 @@ const Bar = styled.div`
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
user-select: none;
height: 28px;
position: relative;
`;

Expand Down
26 changes: 23 additions & 3 deletions shared/editor/components/ResizeHandle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export const ResizeLeft = styled.div<{ $dragging: boolean }>`
cursor: ew-resize;
position: absolute;
left: -4px;
top: 0;
bottom: 0;
top: 30%;
bottom: 30%;
width: 8px;
user-select: none;
opacity: ${(props) => (props.$dragging ? 1 : 0)};
Expand All @@ -19,7 +19,7 @@ export const ResizeLeft = styled.div<{ $dragging: boolean }>`
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 15%;
height: 25%;
min-height: 20px;
border-radius: 4px;
background: ${s("menuBackground")};
Expand All @@ -41,3 +41,23 @@ export const ResizeRight = styled(ResizeLeft)`
right: 8px;
}
`;

export const ResizeBottom = styled(ResizeLeft)`
cursor: ns-resize;
left: 30%;
right: 30%;
top: initial;
bottom: 8px;
width: auto;
height: 8px;
&:after {
left: 50%;
bottom: 8px;
transform: translateX(-50%);
width: 25%;
height: 6px;
min-width: 20px;
min-height: 0;
}
`;
Loading

0 comments on commit d645915

Please sign in to comment.