feat: Full width images (#4389)

* feat: Full width images

* lint

* fix: Enable TOC overlaid on full size images

* Vendorize react-medium-image-zoom

* tsc

* fix

* Remove body scroll lock
This commit is contained in:
Tom Moor
2022-12-17 17:17:15 -08:00
committed by GitHub
parent f8ba393f7c
commit acf74b83a8
19 changed files with 1415 additions and 104 deletions

View File

@@ -10,13 +10,13 @@ import {
} from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as React from "react";
import ImageZoom, { ImageZoom_Image } from "react-medium-image-zoom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { getDataTransferFiles, getEventFiles } from "../../utils/files";
import { sanitizeUrl } from "../../utils/urls";
import { AttachmentValidation } from "../../validations";
import insertFiles, { Options } from "../commands/insertFiles";
import ImageZoom from "../components/ImageZoom";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import uploadPlaceholderPlugin from "../lib/uploadPlaceholder";
import { ComponentProps, Dispatch } from "../types";
@@ -99,7 +99,7 @@ const uploadPlugin = (options: Options) =>
},
});
const IMAGE_CLASSES = ["right-50", "left-50"];
const IMAGE_CLASSES = ["right-50", "left-50", "full-width"];
const imageSizeRegex = /\s=(\d+)?x(\d+)?$/;
type TitleAttributes = {
@@ -362,7 +362,6 @@ export default class Image extends Node {
return (
<ImageComponent
{...props}
view={this.editor.view}
onClick={this.handleSelect(props)}
onDownload={this.handleDownload(props)}
onChangeSize={this.handleChangeSize(props)}
@@ -475,6 +474,19 @@ export default class Image extends Node {
dispatch(state.tr.setNodeMarkup(selection.from, undefined, attrs));
return true;
},
alignFullWidth: () => (state: EditorState, dispatch: Dispatch) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
}
const attrs = {
...state.selection.node.attrs,
title: null,
layoutClass: "full-width",
};
const { selection } = state;
dispatch(state.tr.setNodeMarkup(selection.from, undefined, attrs));
return true;
},
replaceImage: () => (state: EditorState) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
@@ -584,9 +596,12 @@ const ImageComponent = (
view: EditorView;
}
) => {
const { theme, isSelected, node, isEditable } = props;
const { alt, src, layoutClass } = node.attrs;
const { isSelected, node, isEditable } = props;
const { src, layoutClass } = node.attrs;
const className = layoutClass ? `image image-${layoutClass}` : "image";
const [contentWidth, setContentWidth] = React.useState(
() => document.body.querySelector("#full-width-container")?.clientWidth || 0
);
const [naturalWidth, setNaturalWidth] = React.useState(node.attrs.width);
const [naturalHeight, setNaturalHeight] = React.useState(node.attrs.height);
const [size, setSize] = React.useState({
@@ -596,8 +611,25 @@ const ImageComponent = (
const [sizeAtDragStart, setSizeAtDragStart] = React.useState(size);
const [offset, setOffset] = React.useState(0);
const [dragging, setDragging] = React.useState<DragDirection>();
const documentWidth = props.view?.dom.clientWidth;
const [documentWidth, setDocumentWidth] = React.useState(
props.view?.dom.clientWidth || 0
);
const maxWidth = layoutClass ? documentWidth / 3 : documentWidth;
const isFullWidth = layoutClass === "full-width";
React.useLayoutEffect(() => {
const handleResize = () => {
const contentWidth =
document.body.querySelector("#full-width-container")?.clientWidth || 0;
setContentWidth(contentWidth);
setDocumentWidth(props.view?.dom.clientWidth || 0);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [props.view]);
const constrainWidth = (width: number) => {
const minWidth = documentWidth * 0.1;
@@ -687,52 +719,51 @@ const ImageComponent = (
};
}, [dragging, handlePointerMove, handlePointerUp]);
const style = { width: size.width || "auto" };
const style = isFullWidth
? { width: contentWidth }
: { width: size.width || "auto" };
return (
<div contentEditable={false} className={className}>
<ImageWrapper
isFullWidth={isFullWidth}
className={isSelected || dragging ? "ProseMirror-selectednode" : ""}
onClick={dragging ? undefined : props.onClick}
style={style}
style={{
...style,
...(isFullWidth
? { marginLeft: -(contentWidth - documentWidth) / 2 }
: {}),
}}
>
{!dragging && size.width > 60 && size.height > 60 && (
<Button onClick={props.onDownload}>
<DownloadIcon color="currentColor" />
</Button>
)}
<ImageZoom
image={
{
style,
src: sanitizeUrl(src) ?? "",
alt,
onLoad: (ev: React.SyntheticEvent<HTMLImageElement>) => {
// For some SVG's Firefox does not provide the naturalWidth, in this
// rare case we need to provide a default so that the image can be
// seen and is not sized to 0px
const nw = (ev.target as HTMLImageElement).naturalWidth || 300;
const nh = (ev.target as HTMLImageElement).naturalHeight;
setNaturalWidth(nw);
setNaturalHeight(nh);
<ImageZoom zoomMargin={24}>
<img
style={style}
src={sanitizeUrl(src) ?? ""}
onLoad={(ev: React.SyntheticEvent<HTMLImageElement>) => {
// For some SVG's Firefox does not provide the naturalWidth, in this
// rare case we need to provide a default so that the image can be
// seen and is not sized to 0px
const nw = (ev.target as HTMLImageElement).naturalWidth || 300;
const nh = (ev.target as HTMLImageElement).naturalHeight;
setNaturalWidth(nw);
setNaturalHeight(nh);
if (!node.attrs.width) {
setSize((state) => ({
...state,
width: nw,
}));
}
},
} as ImageZoom_Image
}
defaultStyles={{
overlay: {
backgroundColor: theme.background,
},
}}
shouldRespectMaxDimension
/>
{isEditable && (
if (!node.attrs.width) {
setSize((state) => ({
...state,
width: nw,
}));
}
}}
/>
</ImageZoom>
{isEditable && !isFullWidth && (
<>
<ResizeLeft
onPointerDown={handlePointerDown("left")}
@@ -846,16 +877,23 @@ const Caption = styled.p`
}
`;
const ImageWrapper = styled.div`
const ImageWrapper = styled.div<{ isFullWidth: boolean }>`
line-height: 0;
position: relative;
margin-left: auto;
margin-right: auto;
max-width: 100%;
max-width: ${(props) => (props.isFullWidth ? "initial" : "100%")};
transition-property: width, height;
transition-duration: 150ms;
transition-duration: ${(props) => (props.isFullWidth ? "0ms" : "150ms")};
transition-timing-function: ease-in-out;
touch-action: none;
overflow: hidden;
img {
transition-property: width, height;
transition-duration: ${(props) => (props.isFullWidth ? "0ms" : "150ms")};
transition-timing-function: ease-in-out;
}
&:hover {
${Button} {